В практике разработки на платформе 1С:Предприятие 8.3 часто встречается ситуация, когда при записи документа необходимо автоматически создавать или подбирать характеристики для номенклатуры, указанной в табличной части. Одной из самых серьезных ошибок проектирования в этом случае является выполнение запроса или серверного вызова внутри цикла обхода строк. Рассмотрим, почему это замедляет систему и как правильно переписать такой код для достижения максимальной производительности.
Разберем типичную ситуацию: у нас есть цикл по табличной части Товары, в котором для каждой строки вызывается функция НайтиХарактеристику. Внутри этой функции выполняется Новый Запрос. Если в документе 100 строк, система сделает 100 отдельных обращений к СУБД. Кроме того, если вызов происходит с клиента на сервер, мы получаем 100 «клиент-серверных прыжков», что создает огромные задержки из-за сетевого трафика и накладных расходов на инициализацию контекста.
Проанализируем исходный (неоптимальный) алгоритм:
// ТАК ДЕЛАТЬ НЕ РЕКОМЕНДУЕТСЯ
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
Для каждого Строка Из Объект.Товары Цикл
// Каждый вызов — это задержка
СозданиеХарактеристики(Строка.Номенклатура);
Строка.Характеристика = ПолучитьХарактеристику(Строка.Номенклатура);
КонецЦикла;
КонецПроцедуры
Первое, что мы сделаем — откажемся от вызова серверных функций внутри цикла на клиенте. Всю работу с данными необходимо выполнять в одном серверном контексте. Оптимальнее всего использовать предопределенный обработчик модуля объекта ПередЗаписью (на сервере) или создать одну общую процедуру ЗаполнитьХарактеристикиНаСервере, которая вызовется один раз.
Это позволит нам работать напрямую с коллекциями Объект.Товары и избежать ошибок передачи данных через XDTO, которые возникают при попытке вернуть ТаблицаЗначений с сервера на клиент.
Вместо того чтобы искать характеристику для каждой единицы номенклатуры по отдельности, мы подготовим массив всех ссылок на номенклатуру из документа и выполним один запрос. С помощью ЛЕВОЕ СОЕДИНЕНИЕ мы сопоставим нашу номенклатуру с уже существующими характеристиками в базе данных.
Рассмотрим пример правильного запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| Товары.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_НоменклатураДокумента
|ИЗ
| &ТаблицаТоваров КАК Товары
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_НоменклатураДокумента.Номенклатура КАК Номенклатура,
| ЕСТЬNULL(Характеристики.Ссылка, ЗНАЧЕНИЕ(Справочник.ХарактеристикиНоменклатуры.ПустаяСсылка)) КАК Характеристика
|ИЗ
| ВТ_НоменклатураДокумента КАК ВТ_НоменклатураДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ХарактеристикиНоменклатуры КАК Характеристики
| ПО Характеристики.Владелец = ВТ_НоменклатураДокумента.Номенклатура
| И Характеристики.Наименование = &Наименование";
Запрос.УстановитьПараметр("ТаблицаТоваров", Объект.Товары.ВыгрузитьКолонку("Номенклатура"));
Запрос.УстановитьПараметр("Наименование", Формат(Объект.Дата, "ДФ=гггг_ММ_дд"));
Важный момент: использование временных таблиц (ВТ) позволяет базе данных эффективнее строить план запроса, особенно если строк в документе много.
Выясним, как быстро сопоставить результат запроса со строками документа. Итерировать результат запроса для каждой строки ТЧ — это опять цикл в цикле (хоть и в памяти). Для ускорения используем объект Соответствие (Map). Это позволит нам получать ссылку на характеристику по ключу-номенклатуре со скоростью O(1).
Разберем комплексный алгоритм реализации:
Соответствие, где ключ — Номенклатура, а значение — Ссылка на характеристику.Соответствии нет значения, создаем новую характеристику программно, записываем её и добавляем в кэш.
&НаСервере
Процедура ЗаполнитьХарактеристикиОптимизированно()
МассивНоменклатуры = Объект.Товары.ВыгрузитьКолонку("Номенклатура");
НаименованиеНужное = Формат(Объект.Дата, "ДФ=гггг_ММ_дд");
// 1. Поиск существующих
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Владелец, Ссылка ИЗ Справочник.ХарактеристикиНоменклатуры
ГДЕ Владелец В (&Массив) И Наименование = &Имя";
Запрос.УстановитьПараметр("Массив", МассивНоменклатуры);
Запрос.УстановитьПараметр("Имя", НаименованиеНужное);
КэшХарактеристик = Новый Соответствие;
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
КэшХарактеристик.Вставить(Выборка.Владелец, Выборка.Ссылка);
КонецЦикла;
// 2. Обработка ТЧ
Для Каждого СтрТЧ Из Объект.Товары Цикл
НайденнаяСсылка = КэшХарактеристик[СтрТЧ.Номенклатура];
Если НайденнаяСсылка = Неопределено Тогда
// Создаем новую, если не нашли
НоваяХар = Справочники.ХарактеристикиНоменклатуры.СоздатьЭлемент();
НоваяХар.Владелец = СтрТЧ.Номенклатура;
НоваяХар.Наименование = НаименованиеНужное;
НоваяХар.Записать();
НайденнаяСсылка = НоваяХар.Ссылка;
КэшХарактеристик.Вставить(СтрТЧ.Номенклатура, НайденнаяСсылка);
КонецЕсли;
СтрТЧ.Характеристика = НайденнаяСсылка;
КонецЦикла;
КонецПроцедуры
Проанализируем ситуацию, когда два пользователя одновременно записывают документы с одинаковой номенклатурой. Если мы просто проверяем наличие характеристики запросом, есть риск, что оба процесса «не найдут» её и создадут два дублирующих элемента.
Чтобы этого избежать, рекомендуем устанавливать Блокировку Данных перед выполнением запроса. Это заставит второй процесс дождаться окончания транзакции первого и увидеть уже созданную характеристику.
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.ХарактеристикиНоменклатуры");
ЭлементБлокировки.УстановитьПараметр("Владелец", МассивНоменклатуры);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
Если вам все же необходимо вернуть результат обработки с сервера на клиент (например, в управляемой форме до записи объекта), помните, что тип ТаблицаЗначений не передается между слоями. Проанализируем решение этой ошибки:
Вместо возврата таблицы, преобразуйте результат в Массив Структур. Структуры и Массивы прекрасно сериализуются и доступны на тонком клиенте. Однако, как мы выяснили выше, лучшим решением будет модификация Объект напрямую в серверном контексте процедуры ПередЗаписью, что полностью снимает вопрос передачи данных.
Для ускорения работы 1С при заполнении характеристик мы применили три ключевых приема: пакетную обработку данных (замена множества запросов одним), кэширование в оперативной памяти (использование Соответствие) и минимизацию серверных вызовов. Это позволяет сократить время выполнения операции с нескольких секунд до миллисекунд, особенно в документах с большим количеством строк.