Как оптимизировать создание характеристик в 1С и избавиться от запросов в цикле?

Программист 1С v8.3 (Управляемые формы) Управленческий учет Торговля и дистрибуция
← На главную

В практике разработки на платформе 1С:Предприятие 8.3 часто встречается ситуация, когда при записи документа необходимо автоматически создавать или подбирать характеристики для номенклатуры, указанной в табличной части. Одной из самых серьезных ошибок проектирования в этом случае является выполнение запроса или серверного вызова внутри цикла обхода строк. Рассмотрим, почему это замедляет систему и как правильно переписать такой код для достижения максимальной производительности.

Анализ проблемы: почему запрос в цикле — это плохо?

Разберем типичную ситуацию: у нас есть цикл по табличной части Товары, в котором для каждой строки вызывается функция НайтиХарактеристику. Внутри этой функции выполняется Новый Запрос. Если в документе 100 строк, система сделает 100 отдельных обращений к СУБД. Кроме того, если вызов происходит с клиента на сервер, мы получаем 100 «клиент-серверных прыжков», что создает огромные задержки из-за сетевого трафика и накладных расходов на инициализацию контекста.

Проанализируем исходный (неоптимальный) алгоритм:


// ТАК ДЕЛАТЬ НЕ РЕКОМЕНДУЕТСЯ
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
    Для каждого Строка Из Объект.Товары Цикл
        // Каждый вызов — это задержка
        СозданиеХарактеристики(Строка.Номенклатура); 
        Строка.Характеристика = ПолучитьХарактеристику(Строка.Номенклатура);
    КонецЦикла;
КонецПроцедуры

Шаг 1. Перенос логики на сторону сервера

Первое, что мы сделаем — откажемся от вызова серверных функций внутри цикла на клиенте. Всю работу с данными необходимо выполнять в одном серверном контексте. Оптимальнее всего использовать предопределенный обработчик модуля объекта ПередЗаписью (на сервере) или создать одну общую процедуру ЗаполнитьХарактеристикиНаСервере, которая вызовется один раз.

Это позволит нам работать напрямую с коллекциями Объект.Товары и избежать ошибок передачи данных через XDTO, которые возникают при попытке вернуть ТаблицаЗначений с сервера на клиент.

Шаг 2. Использование пакетного запроса и левого соединения

Вместо того чтобы искать характеристику для каждой единицы номенклатуры по отдельности, мы подготовим массив всех ссылок на номенклатуру из документа и выполним один запрос. С помощью ЛЕВОЕ СОЕДИНЕНИЕ мы сопоставим нашу номенклатуру с уже существующими характеристиками в базе данных.

Рассмотрим пример правильного запроса:


Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ РАЗЛИЧНЫЕ
|	Товары.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_НоменклатураДокумента
|ИЗ
|	&ТаблицаТоваров КАК Товары
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|	ВТ_НоменклатураДокумента.Номенклатура КАК Номенклатура,
|	ЕСТЬNULL(Характеристики.Ссылка, ЗНАЧЕНИЕ(Справочник.ХарактеристикиНоменклатуры.ПустаяСсылка)) КАК Характеристика
|ИЗ
|	ВТ_НоменклатураДокумента КАК ВТ_НоменклатураДокумента
|		ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ХарактеристикиНоменклатуры КАК Характеристики
|		ПО Характеристики.Владелец = ВТ_НоменклатураДокумента.Номенклатура
|			И Характеристики.Наименование = &Наименование";

Запрос.УстановитьПараметр("ТаблицаТоваров", Объект.Товары.ВыгрузитьКолонку("Номенклатура"));
Запрос.УстановитьПараметр("Наименование", Формат(Объект.Дата, "ДФ=гггг_ММ_дд"));

Важный момент: использование временных таблиц (ВТ) позволяет базе данных эффективнее строить план запроса, особенно если строк в документе много.

Шаг 3. Кэширование данных с помощью объекта Соответствие

Выясним, как быстро сопоставить результат запроса со строками документа. Итерировать результат запроса для каждой строки ТЧ — это опять цикл в цикле (хоть и в памяти). Для ускорения используем объект Соответствие (Map). Это позволит нам получать ссылку на характеристику по ключу-номенклатуре со скоростью O(1).

Разберем комплексный алгоритм реализации:

  1. Выгружаем уникальную номенклатуру из табличной части в массив.
  2. Одним запросом находим все существующие характеристики.
  3. Заполняем Соответствие, где ключ — Номенклатура, а значение — Ссылка на характеристику.
  4. Проходим по табличной части: если в Соответствии нет значения, создаем новую характеристику программно, записываем её и добавляем в кэш.

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

Проблема дублей и конкурентного доступа

Проанализируем ситуацию, когда два пользователя одновременно записывают документы с одинаковой номенклатурой. Если мы просто проверяем наличие характеристики запросом, есть риск, что оба процесса «не найдут» её и создадут два дублирующих элемента.

Чтобы этого избежать, рекомендуем устанавливать Блокировку Данных перед выполнением запроса. Это заставит второй процесс дождаться окончания транзакции первого и увидеть уже созданную характеристику.


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.ХарактеристикиНоменклатуры");
ЭлементБлокировки.УстановитьПараметр("Владелец", МассивНоменклатуры);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

Решение проблемы с ошибкой XDTO

Если вам все же необходимо вернуть результат обработки с сервера на клиент (например, в управляемой форме до записи объекта), помните, что тип ТаблицаЗначений не передается между слоями. Проанализируем решение этой ошибки:

Вместо возврата таблицы, преобразуйте результат в Массив Структур. Структуры и Массивы прекрасно сериализуются и доступны на тонком клиенте. Однако, как мы выяснили выше, лучшим решением будет модификация Объект напрямую в серверном контексте процедуры ПередЗаписью, что полностью снимает вопрос передачи данных.

Резюме

Для ускорения работы 1С при заполнении характеристик мы применили три ключевых приема: пакетную обработку данных (замена множества запросов одним), кэширование в оперативной памяти (использование Соответствие) и минимизацию серверных вызовов. Это позволяет сократить время выполнения операции с нескольких секунд до миллисекунд, особенно в документах с большим количеством строк.

← На главную