В процессе разработки бизнес-логики в среде 1С перед программистом часто возникает задача связать две независимые сущности (например, документы ЗаказПокупателя и ВозвратТоваровОтПокупателя) по определенному критерию (например, по договору контрагента) и вывести их в иерархическом виде. При этом идеальным результатом кажется получение таблицы, где в каждой строке основного документа (заказа) в отдельной ячейке лежит готовая мини-таблица с подчиненными документами (возвратами).
Давайте подробно проанализируем эту ситуацию, выясним внутренние ограничения платформы 1С, разберем принцип работы редкой конструкции ПУСТАЯТАБЛИЦА и рассмотрим три надежных, высокопроизводительных способа реализации данной задачи.
Для начала разберем фундаментальное ограничение языка запросов 1С. В отличие от некоторых объектно-ориентированных СУБД, реляционные базы данных, на которых работает 1С, всегда возвращают плоские двумерные таблицы. Платформа 1С транслирует запросы во встроенный диалект SQL, который на физическом уровне не умеет вставлять полноценные наборы записей внутрь одной ячейки строки.
Единственным исключением из этого правила являются табличные части документов или справочников. Когда мы пишем запрос к табличной части (например, Документ.ЗаказПокупателя.Товары), платформа на уровне СУБД выполняет соединение таблиц, но при обработке результата на уровне сервера приложений 1С упаковывает эти строки во вложенный объект типа РезультатЗапроса. Попытка написать собственный вложенный запрос в списке выбора, который возвращал бы коллекцию строк, приведет к синтаксической ошибке.
Иногда в поисках решения разработчики наталкиваются на малоизвестную конструкцию ПУСТАЯТАБЛИЦА. Важно понимать, что этот механизм не предназначен для динамического конструирования таблиц в запросе из других источников данных. Его назначение строго ограничено операциями объединения таблиц (ОБЪЕДИНИТЬ ВСЕ).
Давайте рассмотрим пример. Представим, что нам необходимо объединить в один результат выборку из двух разных документов, у одного из которых есть табличная часть, а у другого её нет. Поскольку структуры объединяемых полей должны строго совпадать, мы обязаны описать пустую структуру вложенной таблицы во второй части запроса. Разберем, как это выглядит на практике:
ВЫБРАТЬ
Ссылка.Ссылка КАК ДокументОбъект,
Ссылка.Товары.(Номенклатура, Количество) КАК Состав
ИЗ
Документ.РеализацияТоваровУслуг КАК Ссылка
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Ссылка.Ссылка,
ПУСТАЯТАБЛИЦА.(Номенклатура, Количество)
ИЗ
Документ.АктВыполненныхРабот КАК Ссылка
Как мы видим из примера, конструкция ПУСТАЯТАБЛИЦА лишь декларирует типы колонок для вложенной таблицы, чтобы менеджер временных таблиц и СУБД смогли построить корректную общую схему данных. Наполнить эту таблицу произвольными данными из стороннего документа (например, возвратами для конкретного заказа) с помощью этой конструкции невозможно.
Что же делать в нашей ситуации? Разберем три практических сценария решения проблемы.
Если конечной целью является вывод информации в отчет или на форму, использование Системы компоновки данных — это самый быстрый и технологически правильный путь. СКД умеет самостоятельно связывать разнородные наборы данных на уровне платформы.
Давайте выполним настройку по шагам:
НаборДанныхЗаказы), который выбирает все заказы за период:
ВЫБРАТЬ
Заказ.Ссылка КАК Заказ,
Заказ.ДоговорКонтрагента КАК Договор
ИЗ
Документ.ЗаказПокупателя КАК Заказ
НаборДанныхВозвраты), выбирающий возвраты:
ВЫБРАТЬ
Возврат.Ссылка КАК Возврат,
Возврат.ДоговорКонтрагента КАК Договор
ИЗ
Документ.ВозвратТоваровОтПокупателя КАК Возврат
НаборДанныхЗаказы, приемником — НаборДанныхВозвраты, а в качестве условий сопоставления выберем поле Договор.Заказ, а внутрь нее поместим детальные записи (или группировку) по полю Возврат.При формировании отчета СКД автоматически выполнит сопоставление данных и выведет возвраты непосредственно внутри строки каждого заказа. Это происходит на уровне платформы крайне эффективно и быстро.
Если данные необходимо обработать программно в коде (например, для заполнения таблицы на форме) и вывести их в виде таблицы значений, где одна из колонок должна хранить вложенные таблицы значений, воспользуемся ручной сборкой.
Обычный перебор строк вложенными циклами работает крайне медленно (за время $O(N \times M)$). Чтобы сделать связывание мгновенным, применим индексацию таблиц значений. Давайте подробно разберем этот алгоритм:
// 1. Получаем заказы и возвраты двумя быстрыми запросами (или одним пакетным)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Заказ.Ссылка КАК Заказ,
| Заказ.ДоговорКонтрагента КАК Договор
|ИЗ
| Документ.ЗаказПокупателя КАК Заказ;
|
|ВЫБРАТЬ
| Возврат.Ссылка КАК Возврат,
| Возврат.ДоговорКонтрагента КАК Договор
|ИЗ
| Документ.ВозвратТоваровОтПокупателя КАК Возврат";
Результаты = Запрос.ВыполнитьПакет();
ТЗ_Заказы = Результаты[0].Выгрузить();
ТЗ_Возвраты = Результаты[1].Выгрузить();
// 2. Создаем индекс по полю связи в таблице возвратов! Это ключевой шаг для производительности
ТЗ_Возвраты.Индексы.Добавить("Договор");
// 3. Подготовим в таблице заказов колонку для хранения вложенных таблиц
ТЗ_Заказы.Колонки.Добавить("ТаблицаВозвратов", Новый ОписаниеТипов("ТаблицаЗначений"));
// 4. Заполняем вложенные таблицы с использованием быстрого поиска НайтиСтроки()
Для Каждого СтрокаЗаказа Из ТЗ_Заказы Цикл
// Создаем структуру для поиска по индексированному полю
Отбор = Новый Структура("Договор", СтрокаЗаказа.Договор);
// Метод НайтиСтроки при наличии индекса отрабатывает мгновенно (за O(1))
НайденныеСтроки = ТЗ_Возвраты.НайтиСтроки(Отбор);
// Создаем локальную таблицу нужной структуры для хранения в этой строке
ЛокальнаяТЗ = ТЗ_Возвраты.СкопироватьКолонки();
Для Каждого СтрокаВозврата Из НайденныеСтроки Цикл
ЗаполнитьЗначенияСвойств(ЛокальнаяТЗ.Добавить(), СтрокаВозврата);
КонецЦикла;
// Помещаем готовую таблицу в ячейку текущей строки заказа
СтрокаЗаказа.ТаблицаВозвратов = ЛокальнаяТЗ;
КонецЦикла;
Использование метода Индексы.Добавить() гарантирует, что даже при десятках тысяч заказов и возвратов связывание произойдет за доли секунды, так как платформа будет использовать хэш-таблицы для мгновенного поиска сопоставимых строк.
Если вам требуется вывести данные в древовидной структуре или последовательно напечатать их в табличный документ, нет необходимости собирать промежуточные таблицы значений в памяти. Язык запросов 1С предоставляет мощный инструмент группировки результатов — ключевое слово ИТОГИ ПО.
Рассмотрим структуру такого запроса:
ВЫБРАТЬ
Заказ.Ссылка КАК Заказ,
Заказ.ДоговорКонтрагента КАК Договор,
Возврат.Ссылка КАК Возврат
ИЗ
Документ.ЗаказПокупателя КАК Заказ
ЛЕВОЕ СОЕДИНЕНИЕ Документ.ВозвратТоваровОтПокупателя КАК Возврат
ПО Заказ.ДоговорКонтрагента = Возврат.ДоговорКонтрагента
ИТОГИ ПО
Заказ
Для вывода такого результата запроса мы используем двухуровневый обход выборки. Давайте проанализируем пример кода для печати или вывода данных:
РезультатЗапроса = Запрос.Выполнить();
// Получаем выборку верхнего уровня (только уникальные заказы)
ВыборкаЗаказы = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаЗаказы.Следующий() Цикл
// Здесь мы обрабатываем строку заказа (например, выводим шапку в макет)
Сообщить("Заказ: " + ВыборкаЗаказы.Заказ + " (Договор: " + ВыборкаЗаказы.Договор + ")");
// Получаем подчиненную выборку детальных записей только для текущего заказа
ВыборкаВозвраты = ВыборкаЗаказы.Выбрать();
Пока ВыборкаВозвраты.Следующий() Цикл
// Проверяем, что левое соединение действительно нашло возврат
Если ЗначениеЗаполнено(ВыборкаВозвраты.Возврат) Тогда
// Выводим подчиненную информацию (строку возврата)
Сообщить(" -> Возврат: " + ВыборкаВозвраты.Возврат);
КонецЕсли;
КонецЦикла;
КонецЦикла;
Этот подход является классическим для реализации печатных форм в среде 1С. Платформа самостоятельно оптимизирует выборку и распределяет подчиненные записи по группировкам верхнего уровня, что исключает избыточное выделение оперативной памяти под создание промежуточных объектов ТаблицаЗначений.
Подведем итоги: напрямую получить таблицу внутри ячейки одной строки запроса мешают фундаментальные ограничения реляционных БД. Однако, используя возможности СКД по связыванию наборов данных, индексирование коллекций в памяти или обход выборок с итогами, мы можем легко и с высокой скоростью решить любые задачи по иерархическому представлению информации.