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