В конфигурации «1С:Управление торговлей 10.3» (как и во многих других решениях на обычных формах, построенных на базе «1С:Предприятие 8.2») механизм дополнительных свойств объектов реализован с использованием плана видов характеристик СвойстваОбъектов и периодического регистра сведений ЗначенияСвойствОбъектов. Часто перед разработчиками возникает задача автоматизации заполнения этих свойств — например, когда требуется скопировать артикул или код номенклатуры в специальное строковое свойство «Код для сайта» для последующей интеграции с интернет-магазином.
В этой статье мы подробно разберем, как правильно решить данную задачу, исправим критические архитектурные ошибки в запросах, которые часто допускают начинающие программисты, и напишем быстрый, надежный и отказоустойчивый код для массового заполнения свойств номенклатуры.
Прежде чем писать код для записи данных, необходимо правильно выбрать целевую номенклатуру, для которой интересующее нас свойство еще не заполнено. Посмотрим на типичную ошибку в запросе, с которой часто начинают разработчики:
ВЫБРАТЬ
Номенклатура.Ссылка,
Номенклатура.Код,
ЗначенияСвойствОбъектов.Свойство,
ЗначенияСвойствОбъектов.Значение
ИЗ
Справочник.Номенклатура КАК Номенклатура,
РегистрСведений.ЗначенияСвойствОбъектов КАК ЗначенияСвойствОбъектов
ГДЕ
ЗначенияСвойствОбъектов.Свойство = &Свойство
Проанализируем этот запрос. Здесь таблицы Справочник.Номенклатура и РегистрСведений.ЗначенияСвойствОбъектов просто перечислены через запятую в секции ИЗ без указания условий связи. В теории баз данных такая операция называется декартовым произведением (Cross Join).
Если в вашей базе данных содержится 10 000 товаров, а в регистре значений свойств хранится 50 000 записей, СУБД попытается сопоставить каждую строку первой таблицы с каждой строкой второй таблицы. В результате на выходе мы получим огромную временную таблицу из 500 000 000 строк! Это гарантированно приведет к зависанию сервера 1С, перегрузке оперативной памяти и критическому падению производительности всей системы.
Для решения нашей задачи необходимо использовать левое соединение. Нам нужно получить всю номенклатуру (за исключением папок и помеченных на удаление позиций), у которой в регистре сведений отсутствует запись по конкретному свойству (например, «КодДляСайта»).
Рассмотрим, как правильно настроить связи в запросе:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
Номенклатура.Код КАК Код
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЗначенияСвойствОбъектов КАК ЗначенияСвойствОбъектов
ПО Номенклатура.Ссылка = ЗначенияСвойствОбъектов.Объект
И ЗначенияСвойствОбъектов.Свойство = &Свойство
ГДЕ
ЗначенияСвойствОбъектов.Свойство ЕСТЬ NULL
И НЕ Номенклатура.ЭтоГруппа
И НЕ Номенклатура.ПометкаУдаления
Обратите внимание на два важнейших нюанса в этом запросе:
ГДЕ, СУБД сначала бы выполнила соединение, а затем отфильтровала результат, оставив только строки с заполненным свойством. Это превратило бы наше левое соединение во внутреннее (Inner Join), и мы никогда бы не получили товары с незаполненными свойствами.ЗначенияСвойствОбъектов.Свойство ЕСТЬ NULL гарантирует, что в выборку попадут только те товары, для которых в регистре вообще нет записи по нашему свойству.НЕ Номенклатура.ЭтоГруппа и НЕ Номенклатура.ПометкаУдаления избавляют нас от бессмысленной работы по записи свойств для папок справочника и объектов, подготовленных к удалению из базы.Перед тем как записывать данные, выясним одну очень важную деталь. Какого типа наше дополнительное свойство? В конфигурации «Управление торговлей 10.3» дополнительные свойства могут принимать значения различных типов:
ЗначенияСвойствОбъектов.Если тип свойства в Плане видов характеристик настроен как ссылка на справочник ЗначенияСвойствОбъектов, мы не можем просто записать туда строковый код номенклатуры. Платформа либо очистит это поле, либо вызовет ошибку несоответствия типов. Нам потребуется сначала программно найти или создать элемент в этом подчиненном справочнике, а уже затем записать ссылку на него в регистр сведений.
Рассмотрим программный код, который учитывает обе эти ситуации:
// Очистим окно сообщений перед запуском
ОчиститьОкноСообщений();
// Шаг 1. Получаем свойство, которое мы хотим заполнить
СвойствоКодДляСайта = ПланыВидовХарактеристик.СвойстваОбъектов.НайтиПоНаименованию("Код для сайта");
Если СвойствоКодДляСайта.Пустая() Тогда
Сообщить("Свойство 'Код для сайта' не найдено в базе данных! Проверьте наименование.", СтатусСообщения.Важное);
Возврат;
КонецЕсли;
// Проверяем тип значения свойства
ТипЗначенияСвойства = СвойствоКодДляСайта.ТипЗначения;
НужноСоздаватьЗначениеСправочника = ТипЗначенияСвойства.СодержитТип(Тип("СправочникСсылка.ЗначенияСвойствОбъектов"));
// Шаг 2. Подготовка и выполнение оптимального запроса
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЗначенияСвойствОбъектов КАК ЗначенияСвойствОбъектов
| ПО Номенклатура.Ссылка = ЗначенияСвойствОбъектов.Объект
| И ЗначенияСвойствОбъектов.Свойство = &Свойство
|ГДЕ
| ЗначенияСвойствОбъектов.Свойство ЕСТЬ NULL
| И НЕ Номенклатура.ЭтоГруппа
| И НЕ Номенклатура.ПометкаУдаления";
Запрос.УстановитьПараметр("Свойство", СвойствоКодДляСайта);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Сообщить("Все свойства номенклатуры уже заполнены. Нет данных для обработки.");
Возврат;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
Если мы будем записывать каждую строку в регистр по отдельности, СУБД на каждую итерацию цикла будет инициировать транзакцию записи, делать физическую запись на диск и закрывать транзакцию. На базе данных в 20 000 товаров такая процедура может занять до получаса!
Чтобы ускорить процесс в десятки раз, мы объединим всю операцию записи в одну большую транзакцию с помощью методов НачатьТранзакцию() и ЗафиксироватьТранзакцию(). В случае возникновения непредвиденной ошибки мы сможем безопасно откатить изменения с помощью ОтменитьТранзакцию().
Допишем обработку выборки с использованием транзакций и менеджера записи регистра сведений:
// Шаг 3. Запуск транзакции для максимального ускорения записи
НачатьТранзакцию();
Попытка
СчетчикЗаписей = 0;
Пока Выборка.Следующий() Цикл
// Определяем значение для записи в зависимости от типа свойства
ЗначениеДляЗаписи = Неопределено;
Если НужноСоздаватьЗначениеСправочника Тогда
// Ищем значение в справочнике "Значения свойств объектов" по коду номенклатуры (владелец - наше свойство)
СсылкаНаЗначение = Справочники.ЗначенияСвойствОбъектов.НайтиПоНаименованию(Выборка.Код, Истина, , СвойствоКодДляСайта);
// Если значение еще не создано, создаем его программно
Если СсылкаНаЗначение.Пустая() Тогда
НовыйЭлементСвойства = Справочники.ЗначенияСвойствОбъектов.СоздатьЭлемент();
НовыйЭлементСвойства.Владелец = СвойствоКодДляСайта;
НовыйЭлементСвойства.Наименование = Выборка.Код;
НовыйЭлементСвойства.Записать();
СсылкаНаЗначение = НовыйЭлементСвойства.Ссылка;
КонецЕсли;
ЗначениеДляЗаписи = СсылкаНаЗначение;
Иначе
// Если свойство просто строковое, записываем код напрямую
ЗначениеДляЗаписи = Выборка.Код;
КонецЕсли;
// Используем МенеджерЗаписи для работы с одной записью независимого регистра сведений
МенеджерЗаписи = РегистрыСведений.ЗначенияСвойствОбъектов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Объект = Выборка.Ссылка;
МенеджерЗаписи.Свойство = СвойствоКодДляСайта;
МенеджерЗаписи.Значение = ЗначениеДляЗаписи;
// Записываем с замещением существующих записей (если они вдруг появились)
МенеджерЗаписи.Записать(Истина);
СчетчикЗаписей = СчетчикЗаписей + 1;
// Каждые 1000 элементов будем информировать пользователя
Если СчетчикЗаписей % 1000 = 0 Тогда
Сообщить("Обработано позиций номенклатуры: " + СчетчикЗаписей);
КонецЕсли;
КонецЦикла;
// Фиксируем все изменения в базе данных
ЗафиксироватьТранзакцию();
Сообщить("Успешно заполнено свойств для товаров: " + СчетчикЗаписей, СтатусСообщения.Информация);
Исключение
// В случае ошибки отменяем все транзакции, чтобы не оставить базу в несогласованном состоянии
ОтменитьТранзакцию();
Сообщить("Произошла критическая ошибка при записи: " + ОписаниеОшибки(), СтатусСообщения.Важное);
КонецПопытки;
Мы спроектировали и написали надежный скрипт для заполнения дополнительных свойств. Давайте вспомним ключевые правила, которые мы применили:
ЛЕВОЕ СОЕДИНЕНИЕ.ПО, чтобы не терять строки с незаполненными значениями (проверяя их на NULL в ГДЕ).ЗначенияСвойствОбъектов.НачатьТранзакцию() и ЗафиксироватьТранзакцию() при пакетной записи больших массивов данных. Это ускорит вашу обработку в 10–50 раз и защитит базу данных от незавершенных транзакций при сбоях.