При выгрузке данных в форматы обмена (например, XML или JSON) разработчики часто сталкиваются с задачей: необходимо получить заголовочную часть документа и строки сразу нескольких независимых табличных частей (например, Товары и Услуги). При этом важно организовать удобный иерархический обход без лишней нагрузки на базу данных и сервер.
Попытка соединить несколько табличных частей в одном простом запросе через ЛЕВОЕ СОЕДИНЕНИЕ по полю Ссылка приводит к классической проблеме — декартовому произведению строк. Если в документе содержится 10 строк с товарами и 5 строк с услугами, плоское соединение сформирует 50 строк выборки, дублируя данные шапки и табличных частей. Разберем правильные и эффективные способы решения этой задачи в платформе 1С:Предприятие.
Платформа 1С поддерживает специальный синтаксис запросов, позволяющий извлекать данные табличных частей непосредственно в составе выборки родительского документа с помощью конструкции ИмяТабличнойЧасти.(Поля). В этом случае в результат запроса табличная часть попадает как отдельное поле специального типа.
Посмотрим на пример текста запроса, где выбираются реквизиты шапки и конкретные примитивные поля связанных справочников из двух разных табличных частей:
ВЫБРАТЬ
РеализацияТоваровУслуг.Номер КАК НомерДокумента,
РеализацияТоваровУслуг.Дата КАК ДатаДокумента,
РеализацияТоваровУслуг.Контрагент.Наименование КАК КонтрагентНаименование,
РеализацияТоваровУслуг.Товары.(
Номенклатура.Наименование КАК НаименованиеТовара,
Номенклатура.Артикул КАК АртикулТовара,
Количество КАК Количество,
Сумма КАК Сумма
) КАК ТабличнаяЧастьТовары,
РеализацияТоваровУслуг.Услуги.(
Номенклатура.Наименование КАК НаименованиеУслуги,
Количество КАК Количество,
Сумма КАК Сумма
) КАК ТабличнаяЧастьУслуги
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Проведен = ИСТИНА
Разберем подробнее, как устроен обход полученных данных во встроенном языке. При таком синтаксисе поля ТабличнаяЧастьТовары и ТабличнаяЧастьУслуги внутри текущей строки документа имеют тип РезультатЗапроса. Чтобы обойти строки табличной части, мы вызываем метод Выбрать() непосредственно у соответствующего поля:
Результат = Запрос.Выполнить();
ВыборкаДокумент = Результат.Выбрать();
Пока ВыборкаДокумент.Следующий() Цикл
// Чтение данных шапки документа
Номер = ВыборкаДокумент.НомерДокумента;
ДатаДок = ВыборкаДокумент.ДатаДокумента;
Контрагент = ВыборкаДокумент.КонтрагентНаименование;
// Вложенный обход строк табличной части "Товары"
ВыборкаТовары = ВыборкаДокумент.ТабличнаяЧастьТовары.Выбрать();
Пока ВыборкаТовары.Следующий() Цикл
ТоварНаименование = ВыборкаТовары.НаименованиеТовара;
ТоварКоличество = ВыборкаТовары.Количество;
КонецЦикла;
// Вложенный обход строк табличной части "Услуги"
ВыборкаУслуги = ВыборкаДокумент.ТабличнаяЧастьУслуги.Выбрать();
Пока ВыборкаУслуги.Следующий() Цикл
УслугаНаименование = ВыборкаУслуги.НаименованиеУслуги;
УслугаСумма = ВыборкаУслуги.Сумма;
КонецЦикла;
КонецЦикла;
Рассмотрим ограничения, присущие механизму вложенных таблиц, которые важно учитывать при проектировании архитектуры:
ПОМЕСТИТЬ.СГРУППИРОВАТЬ ПО для объединения данных шапки.УПОРЯДОЧИТЬ ПО; порядок строк сохраняется в соответствии с системным полем НомерСтроки.РезультатЗапроса, что при выборке десятков тысяч документов может создавать высокую нагрузку на память сервера 1С.Если требуется обойти ограничения вложенных таблиц и получить единый плоский набор данных с многоуровневым иерархическим обходом, применим объединение строк с искусственным разделением типов записей.
Проанализируем структуру такого запроса:
ВЫБРАТЬ
Реализация.Ссылка КАК ДокументСсылка,
Реализация.Номер КАК НомерДокумента,
"Товары" КАК ИмяТЧ,
РеализацияТовары.Номенклатура.Наименование КАК Наименование,
РеализацияТовары.Количество КАК Количество
ИЗ
Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
ПО РеализацияТовары.Ссылка = Реализация.Ссылка
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Реализация.Ссылка,
Реализация.Номер,
"Услуги",
РеализацияУслуги.Номенклатура.Наименование,
РеализацияУслуги.Количество
ИЗ
Документ.РеализацияТоваровУслуг.Услуги КАК РеализацияУслуги
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
ПО РеализацияУслуги.Ссылка = Реализация.Ссылка
ИТОГИ ПО
ДокументСсылка,
ИмяТЧ
Для чтения результатов такого запроса используется иерархический обход по уровням группировок:
Результат = Запрос.Выполнить();
// 1 уровень: Документ
ВыборкаДокумент = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаДокумент.Следующий() Цикл
НомерДок = ВыборкаДокумент.НомерДокумента;
// 2 уровень: Табличная часть (Товары или Услуги)
ВыборкаТЧ = ВыборкаДокумент.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "ИмяТЧ");
Пока ВыборкаТЧ.Следующий() Цикл
ТипТЧ = ВыборкаТЧ.ИмяТЧ;
// 3 уровень: Детальные строки конкретной ТЧ
ВыборкаСтрок = ВыборкаТЧ.Выбрать();
Пока ВыборкаСтрок.Следующий() Цикл
ИмяПозиции = ВыборкаСтрок.Наименование;
Кол = ВыборкаСтрок.Количество;
КонецЦикла;
КонецЦикла;
КонецЦикла;
Когда требуется обработать большой объем документов, оптимальным с точки зрения скорости работы СУБД и расхода оперативной памяти является выполнение пакетного запроса через менеджер временных таблиц:
ВТ_Документы.ВТ_Документы.Товары, соединяя их с ВТ_Документы.Услуги.Вызов метода ВыполнитьПакет() возвращает массив результатов, каждый из которых можно быстро сопоставить (например, загрузив строки в таблицы значений с индексацией по ссылке или сформировав структуру соответствий). Этот метод полностью исключает декартово произведение и не порождает вложенных объектов выборок.
Для максимального быстродействия не выгружайте результаты запроса в промежуточный объект ТаблицаЗначений. Используйте потоковую запись через объект ЗаписьXML:
ЗаписьXML = Новый ЗаписьXML;
ЗаписьXML.ОткрытьФайл(ПутьКФайлу, "UTF-8");
ЗаписьXML.ЗаписатьОбъявлениеXML();
ЗаписьXML.ЗаписатьНачалоЭлемента("ДанныеОбмена");
Пока ВыборкаДокумент.Следующий() Цикл
ЗаписьXML.ЗаписатьНачалоЭлемента("Документ");
ЗаписьXML.ЗаписатьАтрибут("Номер", Строка(ВыборкаДокумент.НомерДокумента));
ЗаписьXML.ЗаписатьНачалоЭлемента("Товары");
ВыборкаТовары = ВыборкаДокумент.ТабличнаяЧастьТовары.Выбрать();
Пока ВыборкаТовары.Следующий() Цикл
ЗаписьXML.ЗаписатьНачалоЭлемента("Товар");
ЗаписьXML.ЗаписатьАтрибут("Наименование", Строка(ВыборкаТовары.НаименованиеТовара));
ЗаписьXML.ЗаписатьКонецЭлемента(); // Товар
КонецЦикла;
ЗаписьXML.ЗаписатьКонецЭлемента(); // Товары
ЗаписьXML.ЗаписатьКонецЭлемента(); // Документ
КонецЦикла;
ЗаписьXML.ЗаписатьКонецЭлемента(); // ДанныеОбмена
ЗаписьXML.Закрыть();
Такой подход обеспечивает минимальное потребление оперативной памяти даже при выгрузке сотен тысяч документов, так как данные не накапливаются в памяти, а сразу последовательно записываются на диск.