Как правильно обновить табличную часть документа в 1С 8.3 и избежать появления лишних строк

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

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

Анализ исходного кода и выявление критических ошибок

Рассмотрим код, который изначально пытался использовать автор темы для обновления колонки КоличествоУвеличения в табличной части Запасы. На стороне клиента выполнялся обход строк табличной части в цикле, внутри которого для каждого товара вызывалась серверная функция:


// Код на клиенте
Для Каждого ТекущаяСтрокаТЧ из Объект.Запасы Цикл
    НоменклатураИЗТЧ = ТекущаяСтрокаТЧ.Номенклатура;
    ТекущаяСтрокаТЧ.КоличествоУвеличения = ОбновитьКолонкуУвеличениеНаСервере(НоменклатураИЗТЧ);
КонецЦикла;

// Код на сервере
&НаСервере
Функция ОбновитьКолонкуУвеличениеНаСервере(НоменклатураИЗТЧ)    
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЗаявкиНаПеремещениеОстатки.КоличествоПревышенияОстаток КАК КоличествоПревышенияОстаток,
    |    ЗаявкиНаПеремещениеОстатки.ЕдИзм КАК ЕдИзм,
    |    ЗаявкиНаПеремещениеОстатки.Номенклатура КАК Номенклатура
    |ИЗ
    |    РегистрНакопления.ЗаявкиНаПеремещение.Остатки(, СсылкаНаДок = &Ссылка И ПревышенЛимит = ИСТИНА) КАК ЗаявкиНаПеремещениеОстатки";
    Запрос.УстановитьПараметр("Ссылка", Объект.Ссылка);
    
    РезультатЗапроса = Запрос.Выполнить();        
    Выборка = РезультатЗапроса.Выгрузить();
    
    Для Каждого стр Из Выборка Цикл  
        Если стр.Номенклатура <> НоменклатураИЗТЧ Тогда
            НоваяСтрока = Объект.Запасы.Добавить();
            НоваяСтрока.Номенклатура = стр.Номенклатура;
            НоваяСтрока.КоличествоУвеличения = стр.КоличествоПревышенияОстаток;
            НоваяСтрока.ЕдиницаИзмерения = стр.ЕдИзм;  
        Иначе
            Возврат стр.КоличествоПревышенияОстаток;
        КонецЕсли;       
    КонецЦикла;
КонецФункции

Проанализируем эту ситуацию подробно. Здесь допущены сразу две серьезные архитектурные ошибки, которые делают код неработоспособным и крайне медленным.

Ошибка №1: Запрос в цикле на сервере (Антипаттерн производительности)
Каждый раз, когда на клиенте выполняется итерация по строкам табличной части, система совершает контекстный серверный вызов. Внутри этого вызова заново создается объект Запрос, компилируется текст запроса и отправляется к СУБД. Если в документе содержится хотя бы 50–100 строк, это приведет к огромным задержкам из-за постоянной сетевой сериализации данных формы между клиентом и сервером (так называемый context switch) и избыточной нагрузки на базу данных.

Ошибка №2: Логическое несоответствие при сравнении номенклатуры
Посмотрим на цикл обхода выборки на сервере. При вызове функции передается только одно значение — НоменклатураИЗТЧ. Внутри функции выполняется запрос, который возвращает таблицу остатков по всей номенклатуре документа (так как в параметрах виртуальной таблицы остатков нет фильтра по конкретному товару, только по ссылке на документ). В цикле серверной функции происходит сравнение каждой строки выборки запроса с переданной номенклатурой:

Если стр.Номенклатура <> НоменклатураИЗТЧ Тогда

Если в выборке запроса содержится несколько различных товаров (например, Товар 1, Товар 2, Товар 3), а на проверку пришла номенклатура Товар 1, то для Товар 2 и Товар 3 условие неравенства вернет значение Истина. В результате в табличную часть будут ошибочно добавлены новые строки с Товар 2 и Товар 3. При обработке следующей строки на клиенте ситуация повторится, что приведет к созданию сотен дублирующих строк.

Решение №1: Пакетное обновление ТЧ за один серверный вызов (Рекомендуемый способ)

Для исправления ситуации необходимо полностью отказаться от цикла на клиенте. Перенесем всю логику на сторону сервера и выполним обработку данных за один вызов. Для поиска существующих позиций номенклатуры в табличной части будем использовать быстрый метод поиска НайтиСтроки().

Разберем этот алгоритм по шагам:

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

Посмотрим на пример практической реализации данного метода:


&НаКлиенте
Процедура ОбновитьКолонкуУвеличение()
    ОбновитьКолонкуУвеличениеНаСервере();
КонецПроцедуры

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

Решение №2: Использование временных таблиц и ЛЕВОГО СОЕДИНЕНИЯ (Для больших табличных частей)

Если табличная часть документа содержит сотни или тысячи строк, поиск методом НайтиСтроки() в цикле может работать недостаточно оптимально. В таких случаях лучшей практикой в 1С является перенос сопоставления данных на сторону СУБД с помощью временных таблиц и левого соединения.

Рассмотрим подробнее данный сценарий:

  1. Выгружаем текущую табличную часть документа во временную таблицу в запросе.
  2. Соединяем ее левым соединением с таблицей остатков из регистра по полю Номенклатура.
  3. Получаем готовый набор данных, содержащий как обновленные строки, так и новые позиции, которых еще не было в ТЧ.
  4. Очищаем исходную табличную часть и загружаем в нее результат выполнения запроса методом Загрузить().

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


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

Решение №3: Оптимизация через внеконтекстный вызов &НаСервереБезКонтекста

В случаях, когда форма документа сильно перегружена элементами, реквизитами и файлами, передача контекста всей формы на сервер может негативно сказываться на отзывчивости интерфейса. Для решения этой проблемы мы можем использовать внеконтекстный серверный вызов &НаСервереБезКонтекста.

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


&НаКлиенте
Процедура ОбновитьКолонкуБезКонтекста()
    
    // Собираем массив уникальной номенклатуры из ТЧ для передачи на сервер
    МассивНоменклатуры = Новый Массив;
    Для Каждого СтрокаТЧ Из Объект.Запасы Цикл
        Если МассивНоменклатуры.Найти(СтрокаТЧ.Номенклатура) = Неопределено Тогда
            МассивНоменклатуры.Добавить(СтрокаТЧ.Номенклатура);
        EndIf;
    КонецЦикла;
    
    СсылкаНаДокумент = Объект.Ссылка;
    
    // Получаем соответствие с сервера за один легкий вызов
    ДанныеОстатков = ПолучитьДанныеОстатковНаСервере(СсылкаНаДокумент, МассивНоменклатуры);
    
    // Обновляем показатели на клиенте
    Для Каждого СтрокаТЧ Из Объект.Запасы Цикл
        НайденноеЗначение = ДанныеОстатков.Получить(СтрокаТЧ.Номенклатура);
        Если НайденноеЗначение <> Неопределено Тогда
            СтрокаТЧ.КоличествоУвеличения = НайденноеЗначение;
        КонецЕсли;
    КонецЦикла;
    
КонецПроцедуры

&НаСервереБезКонтекста
Функция ПолучитьДанныеОстатковНаСервере(СсылкаНаДокумент, МассивНоменклатуры)
    
    РезультатСоответствие = Новый Соответствие;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЗаявкиНаПеремещениеОстатки.КоличествоПревышенияОстаток КАК КоличествоПревышенияОстаток,
    |    ЗаявкиНаПеремещениеОстатки.Номенклатура КАК Номенклатура
    |ИЗ
    |    РегистрНакопления.ЗаявкиНаПеремещение.Остатки(
    |            ,
    |            СсылкаНаДок = &Ссылка
    |                И Номенклатура В (&МассивНоменклатуры)
    |                И ПревышенЛимит = ИСТИНА) КАК ЗаявкиНаПеремещениеОстатки";
    
    Запрос.УстановитьПараметр("Ссылка", СсылкаНаДокумент);
    Запрос.УстановитьПараметр("МассивНоменклатуры", МассивНоменклатуры);
    
    Выборка = Запрос.Выполнить().Выбрать();
    Пока Выборка.Следующий() Цикл
        РезультатСоответствие.Вставить(Выборка.Номенклатура, Выборка.КоличествоПревышенияОстаток);
    КонецЦикла;
    
    Возврат РезультатСоответствие;
    
КонецФункции

Важные нюансы обновления данных в управляемых формах

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

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

← На главную