В платформе «1С:Предприятие 7.7» отсутствует встроенная поддержка хранения вложенных таблиц в результатах запроса, как это реализовано в более современных версиях 1С 8.x. Тем не менее, перед разработчиками часто возникает задача выгрузить иерархическую структуру данных (например, список складов, где для каждого склада внутри строки должна храниться его контактная информация).
В этой статье мы подробно разберем, как решить эту задачу на платформе 1С 7.7. Мы рассмотрим несколько эффективных архитектурных подходов: от использования стандартных группировок «черного» запроса до работы с плоскими таблицами и применения профессиональных инструментов прямой работы с базами данных.
Представим, что у нас есть справочник Склады и подчиненный ему (или связанный через реквизит) справочник СП_КонтактнаяИнформация. Нам необходимо получить объект типа ТаблицаЗначений, в котором будут колонки со свойствами склада, а также колонка Контакты, содержащая для каждой строки отдельный независимый объект типа ТаблицаЗначений со списком всех контактов этого склада.
Поскольку реквизиты типа «Таблица значений» в базе данных не хранятся, а сам язык запросов 1С 7.7 выдает только линейный («плоский») результат, нам придется комбинировать возможности запроса с программной постобработкой полученного результата.
---
Это классический способ, доступный в штатном функционале 1С 7.7. Мы пишем один запрос, в котором настраиваем двухуровневую группировку: сначала по складам, а затем по контактной информации. При обходе выборки мы динамически создаем вложенные таблицы значений и наполняем их данными.
Выясним причину, почему этот метод считается наиболее надежным для небольших баз данных. Он позволяет использовать стандартный транслятор запросов 1С и гарантирует правильность выборки данных без привлечения сторонних библиотек.
Рассмотрим подробнее программный код реализации этого метода:
// Создание объекта типа Запрос
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|//{{ЗАПРОС(Сформировать)
|Склады = Справочник.Склады.ТекущийЭлемент;
|КонтИнформ = Справочник.СП_КонтактнаяИнформация.ТекущийЭлемент;
|Владелец = Справочник.СП_КонтактнаяИнформация.Владелец;
|ВидКонтакта = Справочник.СП_КонтактнаяИнформация.ВидКонтактнойИнформации;
|ЗначениеКонтакта = Справочник.СП_КонтактнаяИнформация.КонтактнаяИнформация;
|Группировка Склады;
|Группировка КонтИнформ;
|Условие(Склады = Владелец);
|"//}}ЗАПРОС
;
// Выполняем запрос
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Предупреждение("Ошибка выполнения запроса!");
Возврат;
EndЕсли;
// Создаем главную Таблицу Значений
ТЗ_Главная = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Главная.НоваяКолонка("Склад");
ТЗ_Главная.НоваяКолонка("ТаблицаКонтактов"); // Здесь будут храниться вложенные ТЗ
// Начинаем обход первой группировки (Склады)
Пока Запрос.Группировка("Склады") = 1 Цикл
ТЗ_Главная.НоваяСтрока();
ТЗ_Главная.Склад = Запрос.Склады;
// Для каждого склада создаем локальную Таблицу Значений для контактов
ТЗ_Локальная = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Локальная.НоваяКолонка("ВидКонтакта");
ТЗ_Локальная.НоваяКолонка("ЗначениеКонтакта");
// Начинаем обход подчиненной группировки (Контактная информация)
Пока Запрос.Группировка("КонтИнформ") = 1 Цикл
ТЗ_Локальная.НоваяСтрока();
ТЗ_Локальная.ВидКонтакта = Запрос.ВидКонтакта;
ТЗ_Локальная.ЗначениеКонтакта = Запрос.ЗначениеКонтакта;
КонецЦикла;
// Записываем ссылку на созданную локальную таблицу во флагманскую ТЗ
ТЗ_Главная.ТаблицаКонтактов = ТЗ_Локальная;
КонецЦикла;
Важное примечание: Обратите внимание на строку Условие(Склады = Владелец);. Этот синтаксис в стандартном («черном») запросе 1С 7.7 по умолчанию эмулирует INNER JOIN (внутреннее соединение). Это означает, что если у склада нет ни одной записи контактной информации, то такой склад вообще не попадет в результат выполнения нашего запроса.
---
Что делать, если нам необходимо вывести абсолютно все склады из базы данных, независимо от того, заполнена ли по ним контактная информация? В 1С 7.7 нет явного оператора LEFT JOIN, но мы можем эмулировать его поведение с помощью ключевого слова Все в описании группировки.
Разберем по шагам, как изменить логику запроса:
// Модифицированный текст запроса для имитации левого соединения
ТекстЗапроса = "
|//{{ЗАПРОС(Сформировать)
|Склады = Справочник.Склады.ТекущийЭлемент;
|КонтИнформ = Справочник.СП_КонтактнаяИнформация.ТекущийЭлемент;
|Владелец = Справочник.СП_КонтактнаяИнформация.Владелец;
|ВидКонтакта = Справочник.СП_КонтактнаяИнформация.ВидКонтактнойИнформации;
|ЗначениеКонтакта = Справочник.СП_КонтактнаяИнформация.КонтактнаяИнформация;
|Группировка Склады Все; // Ключевое слово Все принудительно выводит все склады
|Группировка КонтИнформ;
|Условие(Склады = Владелец);
|"//}}ЗАПРОС
;
При использовании ключевого слова Все в цикле обхода первой группировки будут возвращены все элементы справочника Склады. Для тех складов, у которых нет контактной информации, вложенный цикл Пока Запрос.Группировка("КонтИнформ") = 1 просто выполнится ноль раз. В итоге в главной таблице значений у такого склада будет лежать пустая таблица контактов, что полностью соответствует логике левого соединения.
---
Еще один высокопроизводительный метод — это получение «плоской» таблицы значений напрямую из запроса без использования вложенных группировок, с её последующей программной сверткой.
Этот способ работает значительно быстрее на уровне СУБД, так как плоский запрос требует меньше ресурсов для построения планов выполнения. Проанализируем ситуацию: мы выгружаем результат запроса в одну общую таблицу и обрабатываем ее в цикле, отслеживая смену ключевого поля (Склад).
Посмотрим на пример реализации данного алгоритма:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Склад = Справочник.СП_КонтактнаяИнформация.Владелец;
|ВидКонтакта = Справочник.СП_КонтактнаяИнформация.ВидКонтактнойИнформации;
|ЗначениеКонтакта = Справочник.СП_КонтактнаяИнформация.КонтактнаяИнформация;
|Группировка Склад;
|Группировка ВидКонтакта;
|";
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
// Выгружаем плоский результат запроса в Таблицу Значений
ТЗ_Результат = СоздатьОбъект("ТаблицаЗначений");
Запрос.Выгрузить(ТЗ_Результат, 0, 0);
// Сортируем полученную таблицу по складу для корректной свертки в один проход
ТЗ_Результат.Сортировать("Склад");
// Готовим результирующую структуру
ТЗ_Итоговая = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Итоговая.НоваяКолонка("Склад");
ТЗ_Итоговая.НоваяКолонка("ТаблицаКонтактов");
ТекущийСклад = 0;
ТЗ_Локальная = "";
ТЗ_Результат.ВыбратьСтроки();
Пока ТЗ_Результат.ПолучитьСтроку() = 1 Цикл
Если ТЗ_Результат.Склад <> ТекущийСклад Тогда
// Если это новый склад — записываем предыдущий (если он был) в итоговую таблицу
Если ТекущийСклад <> 0 Тогда
ТЗ_Итоговая.НоваяСтрока();
ТЗ_Итоговая.Склад = ТекущийСклад;
ТЗ_Итоговая.ТаблицаКонтактов = ТЗ_Локальная;
КонецЕсли;
// Инициализируем новую локальную таблицу для следующего склада
ТекущийСклад = ТЗ_Результат.Склад;
ТЗ_Локальная = СоздатьОбъект("ТаблицаЗначений");
ТЗ_Локальная.НоваяКолонка("ВидКонтакта");
ТЗ_Локальная.НоваяКолонка("ЗначениеКонтакта");
КонецЕсли;
// Добавляем контактные данные в текущую локальную таблицу
ТЗ_Локальная.НоваяСтрока();
ТЗ_Локальная.ВидКонтакта = ТЗ_Результат.ВидКонтакта;
ТЗ_Локальная.ЗначениеКонтакта = ТЗ_Результат.ЗначениеКонтакта;
КонецЦикла;
// Не забываем добавить последний обработанный склад после окончания цикла
Если ТекущийСклад <> 0 Тогда
ТЗ_Итоговая.НоваяСтрока();
ТЗ_Итоговая.Склад = ТекущийСклад;
ТЗ_Итоговая.ТаблицаКонтактов = ТЗ_Локальная;
КонецЕсли;
Этот алгоритм работает в линейном времени O(N) после сортировки, что делает его крайне эффективным инструментом в арсенале разработчика 1С 7.7 при обработке больших массивов данных.
---
Для крупных баз данных (особенно работающих под управлением MS SQL Server или при использовании локального SQLite через библиотеку 1sqlite) стандартные запросы 1С 7.7 работают неприемлемо медленно. В профессиональной среде разработки для решения таких задач используется внешняя компонента 1С++.
С помощью класса ODBCRecordset мы можем написать полноценный прямой SQL-запрос с использованием конструкции LEFT OUTER JOIN. Это вернет плоскую таблицу значений за миллисекунды. После этого с помощью специального класса ИндексированнаяТаблицаЗначений (ИТЗ) мы можем мгновенно связать и сгруппировать данные.
Посмотрим, как выглядит архитектура прямого SQL-запроса через 1С++:
// Инициализация объекта прямого запроса к СУБД
ЗапросSQL = СоздатьОбъект("ODBCRecordset");
ТекстSQL = "
|SELECT
| Склады.ID as [Склад $Справочник.Склады],
| Контакты.ID as [Контакт $Справочник.СП_КонтактнаяИнформация],
| Контакты.SP_Vid as [ВидКонтакта $Перечисление.ВидыКонтактов],
| Контакты.SP_Value as [ЗначениеКонтакта $Строка]
|FROM
| $Справочник.Склады AS Склады
|LEFT OUTER JOIN
| $Справочник.СП_КонтактнаяИнформация AS Контакты ON Контакты.ParentID = Склады.ID
|";
// Выполняем запрос напрямую в SQL и получаем результат в объект ТаблицаЗначений
ТЗ_Плоская = ЗапросSQL.ВыполнитьИнструкцию(ТекстSQL);
Полученную плоскую таблицу далее можно очень быстро преобразовать во вложенную структуру с помощью методов класса ИндексированнаяТаблицаЗначений. ИТЗ поддерживает быстрое индексирование колонок по оперативной памяти, что избавляет разработчика от необходимости использовать медленные вложенные циклы поиска.
---
Подводя итоги, сопоставим рассмотренные подходы по ключевым критериям:
Выбирайте метод, исходя из требований к производительности вашей системы и используемой инфраструктуры базы данных. Наш совместный разбор показал, что даже в рамках жестких ограничений платформы 1С 7.7 существуют изящные и быстрые способы построения сложных иерархических структур данных.