Приветствуем всех разработчиков, продолжающих поддерживать и развивать решения на легендарной платформе 1С:Предприятие 7.7. Несмотря на триумфальное шествие платформы 1С 8.3, конфигурации на "семерке" все еще активно используются в малом бизнесе и на различных промышленных предприятиях благодаря своей сверхвысокой скорости работы на старом оборудовании и простоте администрирования.
В этой статье мы подробно разберем одну из самых популярных архитектурных проблем встроенного языка запросов 1С 7.7 (так называемого "черного запроса"), с которой сталкиваются программисты при попытке выгрузить иерархический список справочника (например, справочника Номенклатура) вместе с родительскими группами. Выясним, почему у элементов справочника родительские группы заполняются корректно, а у самих групп поле Родитель остается пустым, и рассмотрим несколько эффективных способов решения этой задачи.
---
Рассмотрим типичный пример запроса, который часто пишут разработчики для получения списка номенклатуры с иерархией:
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса =
"//{{ЗАПРОС(Сформировать)
|Обрабатывать НеПомеченныеНаУдаление;
|Без итогов;
|Наименование = Справочник.Номенклатура.ТекущийЭлемент;
|ВидНоменклатуры = Справочник.Номенклатура.ВидНоменклатуры;
|ЕдиницаИзмерения = Справочник.Номенклатура.ЕдиницаИзмерения;
|Родитель = Справочник.Номенклатура.Родитель;
|Группировка Наименование;
|"//}}ЗАПРОС
При выполнении этого запроса и последующем обходе группировки по полю Наименование (которое по сути является текущим элементом справочника) мы сталкиваемся со странным поведением системы:
Родитель возвращает правильную ссылку на родительскую группу.Родитель возвращает пустое значение, хотя у этой группы физически есть родительская папка более высокого уровня.Давайте разберемся, почему так происходит на уровне платформы.
---
Чтобы понять причину, проанализируем внутреннее устройство механизма запросов 1С 7.7. На уровне СУБД (будь то SQL Server или DBF-файлы таблиц) движок запросов 1С 7.7 выбирает исключительно конечные элементы справочника (у которых признак ЭтоГруппа() = 0). Сами группы как отдельные сущности в первичную SQL/DBF-выборку не попадают.
Когда мы выполняем метод обхода результатов запроса Запрос.Группировка() с включенной иерархией, платформа программно на стороне клиента "на лету" достраивает дерево групп для тех элементов, которые вошли в итоговую выборку. Но поскольку сами группы физически не запрашивались базой данных через тело запроса, все связанные переменные запроса (такие как Родитель, Код, ВидНоменклатуры и любые другие реквизиты) для строк-групп остаются неинициализированными (пустыми).
Данные реквизитов корректно заполняются только на конечных элементах.
---
Самое простое и элегантное решение проблемы, не требующее изменения самого текста запроса, заключается в том, чтобы брать родительскую группу не из результатов самого запроса, а напрямую из контекста базы данных при позиционировании на строке-группе.
Давайте посмотрим на практический пример реализации этого метода:
Запрос = СоздатьОбъект("Запрос");
// ... Выполнение запроса ...
Пока Запрос.Группировка("Наименование") = 1 Цикл
// Проверяем, является ли текущая строка группировки группой справочника
Если Запрос.Наименование.ЭтоГруппа() = 1 Тогда
// Это группа. Получаем родителя напрямую через обращение к объекту БД
РодительОбъекта = Запрос.Наименование.Родитель;
Сообщить("Группа: " + Запрос.Наименование + " | Родитель: " + РодительОбъекта);
Иначе
// Это конечный элемент. Берем родителя напрямую из переменной запроса
РодительОбъекта = Запрос.Родитель;
Сообщить("Элемент: " + Запрос.Наименование + " | Родитель: " + РодительОбъекта);
КонецЕсли;
КонецЦикла;
Важное примечание: Обращение вида Запрос.Наименование.Родитель заставляет платформу выполнить дополнительное быстрое обращение к кэшу или базе данных для считывания реквизита конкретного объекта. На небольших и средних справочниках это практически не влияет на производительность.
---
Для оптимизации кода и явного разделения логики обработки элементов и групп можно использовать встроенную функцию объекта Запрос — метод ЭтоГруппа(). Этот метод возвращает значение 1, если текущая строка выборки является группировочной строкой-родителем, созданной иерархией платформы.
Рассмотрим структуру кода с использованием этого метода:
Пока Запрос.Группировка("Наименование") = 1 Цикл
// Передаем имя группировки в метод ЭтоГруппа
Если Запрос.ЭтоГруппа("Наименование") = 1 Тогда
// Строка является группой. Переменные запроса здесь пусты.
// Читаем ТекущийЭлемент напрямую для извлечения родителя
ТекГруппа = Запрос.Наименование; // Получаем ссылку на группу
РодительГруппы = ТекГруппа.Родитель;
// Теперь мы можем работать с родителем группы
Если ПустоеЗначение(РодительГруппы) = 0 Тогда
Сообщить("Родитель группы '" + ТекГруппа + "': " + РодительГруппы);
КонецЕсли;
Иначе
// Обычный элемент. Все переменные запроса заполнены нормально
РодительЭлемента = Запрос.Родитель;
КонецЕсли;
КонецЦикла;
Этот подход делает код более структурированным и понятным для других разработчиков, сопровождающих вашу конфигурацию.
---
Часто в практике программирования 1С результат запроса выгружают в таблицу значений с помощью метода Выгрузить() для последующего вывода в форму, на печать или для программной обработки. При выполнении кода Запрос.Выгрузить(ТЗ, 0, 0) строки, соответствующие группам, перенесутся в таблицу значений, но колонка Родитель для них также будет содержать пустые значения.
Давайте проанализируем, как решить эту задачу с помощью постобработки полученной таблицы значений:
ТЗ = СоздатьОбъект("ТаблицаЗначений");
// Выгружаем результаты запроса в таблицу значений со всей иерархией
Запрос.Выгрузить(ТЗ, 0, 0);
// Производим обход строк ТЗ для заполнения пустых родителей у групп
ТЗ.ВыбратьСтроки();
Пока ТЗ.ПолучитьСтроку() = 1 Цикл
// Если колонка Родитель пуста, проверяем, не группа ли это
Если ПустоеЗначение(ТЗ.Родитель) = 1 Тогда
// Если в качестве группировочного поля использовалось "Наименование" (ТекущийЭлемент)
Если ПолучитьПустоеЗначение("Справочник.Номенклатура") <> ТЗ.Наименование Тогда
Если ТЗ.Наименование.ЭтоГруппа() = 1 Тогда
// Восстанавливаем родителя группы через прямое обращение к базе данных
ТЗ.Родитель = ТЗ.Наименование.Родитель;
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
После такого цикла таблица значений становится полностью заполненной и готовой к дальнейшему использованию (например, для передачи в дерево значений на форму или для выгрузки во внешние файлы).
---
Если вам не требуется накладывать сложные условия по реквизитам подчиненных справочников или подсчитывать итоги, использование механизма запросов в 1С 7.7 для простого получения иерархии может быть избыточным. Объектный обход справочника работает напрямую с физическими таблицами и лишен описанных недостатков "черного запроса".
Проанализируем пример кода с использованием объектного метода:
Спр = СоздатьОбъект("Справочник.Номенклатура");
// Параметр 1 в методе ВыбратьЭлементы() включает иерархический порядок обхода
Спр.ВыбратьЭлементы(1);
Пока Спр.ПолучитьЭлемент() = 1 Цикл
// Метод Спр.Родитель всегда гарантированно возвращает родительскую группу
// как для элементов, так и для групп любого уровня вложенности!
Если Спр.ЭтоГруппа() = 1 Тогда
Сообщить("Группа: " + Спр.Наименование + ", Родитель: " + Спр.Родитель);
Иначе
Сообщить("Элемент: " + Спр.Наименование + ", Родитель: " + Спр.Родитель);
КонецЕсли;
КонецЦикла;
Этот способ работает стабильно, предсказуемо и не требует никаких дополнительных ухищрений для восстановления реквизитов родительских веток дерева.
---
Для разработчиков, привыкших к возможностям современных версий платформы, такое поведение 1С 7.7 может показаться странным багом. На самом деле это просто архитектурная особенность старой версии платформы.
В 1С 8.x язык запросов построен на полноценном декларативном стандарте SQL. Элементы и группы в таблицах СУБД 1С 8 равноправны при выборке данных. Запрос вида:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Ссылка,
Номенклатура.Родитель КАК Родитель
ИЗ
Справочник.Номенклатура КАК Номенклатура
Сразу вернет заполненное поле Родитель как для элементов, так и для всех родительских групп справочника на любом уровне иерархии без написания дополнительного программного кода обхода результатов.