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