Почему в запросе 1С не выводятся реквизиты шапки при обходе по группировкам и как это исправить

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разработка печатных форм и отчетов — одна из самых частых задач программиста 1С. Однако при работе с запросами, содержащими итоги и группировки, разработчики регулярно сталкиваются с неприятным поведением платформы: реквизиты шапки документа (например, НомерГруппы, Ответственный, Организация) выводятся пустыми или принимают значение NULL. При этом в консоли запросов данные могут отображаться корректно.

В этой статье мы подробно проанализируем, почему возникает эта проблема, разберем внутренние механизмы работы выборок 1С и рассмотрим несколько надежных способов решения задачи в соответствии со стандартами разработки фирмы «1С».

Выясняем причину: почему реквизиты возвращают NULL?

Рассмотрим классическую ситуацию. У нас есть документ Группы, у которого есть табличная часть Курсанты. Нам нужно вывести печатную форму, сгруппированную по филиалам (месту работы курсантов), а в шапке печатной формы отобразить общие реквизиты документа: НомерГруппы и Ответственный.

В исходном (проблемном) коде запрос выглядел следующим образом:


ВЫБРАТЬ
    ГруппыКурсанты.Курсанты.Наименование КАК Курсанты,
    ГруппыКурсанты.Курсанты.МестоРаботыКурсанта.Наименование КАК Филиал,
    Группы.Ответственный,
    Группы.НомерГруппы
ИЗ
    Документ.Группы.Курсанты КАК ГруппыКурсанты
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Группы КАК Группы
    ПО ГруппыКурсанты.Ссылка = Группы.Ссылка
ГДЕ
    Группы.Ссылка В (&Ссылка)
УПОРЯДОЧИТЬ ПО
    Филиал,
    Курсанты
ИТОГИ ПО
    Филиал

При обходе результата этого запроса с помощью конструкции Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) разработчик пытается заполнить параметры шапки до начала обхода детальных записей:


ВыборкаФилиалы = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
// Попытка заполнить параметры шапки
ОбластьЗаголовок.Параметры.Заполнить(ВыборкаФилиалы); 
ТабДок.Вывести(ОбластьЗаголовок);

Здесь кроются сразу две серьезные ошибки:

  1. Обращение к выборке до вызова метода Следующий(). Сразу после создания объекта ВыборкаИзРезультатаЗапроса указатель находится в позиции «перед первой записью». В этот момент выборка не содержит никаких данных. Любая попытка прочитать свойства или вызвать метод Заполнить() приведет к тому, что параметры макета останутся пустыми. Чтобы получить доступ к данным первого уровня группировки, необходимо хотя бы один раз вызвать ВыборкаФилиалы.Следующий().
  2. Особенности работы секции ИТОГИ ПО в платформе 1С:Предприятие. Это ключевая причина. Когда мы используем предложение ИТОГИ ПО Филиал, платформа строит иерархическую структуру данных. На верхнем уровне (уровень группировки Филиал) заполняются только те поля, которые указаны в секции ПО (в нашем случае это Филиал), и поля, к которым применены агрегатные функции (например, СУММА, КОЛИЧЕСТВО) в секции ИТОГИ. Все остальные поля детальных записей, включая реквизиты шапки документа Ответственный и НомерГруппы, на уровне этой группировки физически равны NULL. Они появляются только тогда, когда мы спускаемся на уровень ниже — к детальным записям (курсантам).

Рассмотрим подробнее четыре способа исправления этой архитектурной проблемы.

Способ 1. Использование пакетного запроса (Рекомендуемый стандарт БСП)

По стандартам разработки фирмы «1С», используемым в Библиотеке стандартных подсистем (БСП), смешивать получение данных шапки и табличной части в одном запросе с итогами не рекомендуется. Гораздо эффективнее и чище разделить получение данных на два независимых шага в рамках одного пакета запросов.

Разберем этот вариант по шагам. Первый запрос пакета будет выбирать только реквизиты шапки документа по ссылке. Второй запрос пакета выберет детальные записи табличной части с группировкой по филиалам.


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |	Группы.Ответственный КАК Ответственный,
    |	Группы.НомерГруппы КАК НомерГруппы
    |ИЗ
    |	Документ.Группы КАК Группы
    |ГДЕ
    |	Группы.Ссылка = &Ссылка
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |	ГруппыКурсанты.Курсанты.Наименование КАК Курсанты,
    |	ГруппыКурсанты.Курсанты.МестоРаботыКурсанта.Наименование КАК Филиал
    |ИЗ
    |	Документ.Группы.Курсанты КАК ГруппыКурсанты
    |ГДЕ
    |	ГруппыКурсанты.Ссылка = &Ссылка
    |УПОРЯДОЧИТЬ ПО
    |	Филиал,
    |	Курсанты
    |ИТОГИ ПО
    |	Филиал";

Запрос.УстановитьПараметр("Ссылка", Ссылка);

// Выполняем пакет запросов
РезультатПакета = Запрос.ВыполнитьПакет();

// 1. Выводим заголовок из первого запроса пакета
ВыборкаШапка = РезультатПакета[0].Выбрать();
Если ВыборкаШапка.Следующий() Тогда
    ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
    ОбластьЗаголовок.Параметры.Заполнить(ВыборкаШапка);
    ТабДок.Вывести(ОбластьЗаголовок);
КонецЕсли;

// 2. Выводим группировки и детальные записи из второго запроса пакета
ВыборкаФилиалы = РезультатПакета[1].Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

ОбластьФилиалы = Макет.ПолучитьОбласть("Курсанты");
ОбластьСписок = Макет.ПолучитьОбласть("Список");
ОбластьПодвал = Макет.ПолучитьОбласть("Низ");

Пока ВыборкаФилиалы.Следующий() Цикл
    // Выводим группировку по Филиалу
    ОбластьФилиалы.Параметры.Заполнить(ВыборкаФилиалы);
    ТабДок.Вывести(ОбластьФилиалы);
    
    СтрокаКурсантов = "";
    // Получаем детальные записи по конкретному филиалу
    ВыборкаДетали = ВыборкаФилиалы.Выбрать();
    Пока ВыборкаДетали.Следующий() Цикл
        СтрокаКурсантов = СтрокаКурсантов + ВыборкаДетали.Курсанты + ", ";
    КонецЦикла;
    
    // Форматируем строку и выводим список курсантов
    Если ЗначениеЗаполнено(СтрокаКурсантов) Тогда
        СтрокаКурсантов = Лев(СтрокаКурсантов, СтрДлина(СтрокаКурсантов) - 2) + ".";
    КонецЕсли;
    
    ОбластьСписок.Параметры.Курсанты = СтрокаКурсантов;
    ТабДок.Вывести(ОбластьСписок);
КонецЦикла;

// Выводим подвал печатной формы
ОбластьПодвал.Параметры.Заполнить(ВыборкаШапка); // Параметры подвала (например, Ответственный) берем из шапки
ТабДок.Вывести(ОбластьПодвал);

Преимущества данного подхода очевидны: мы избавляемся от лишних значений NULL, код становится линейным, простым для понимания и отладки, а производительность запроса возрастает за счет отсутствия избыточных соединений.

Способ 2. Поднятие реквизитов на уровень группировок через агрегатные функции

Если вам по какой-то причине необходимо решить задачу строго в рамках одного запроса, можно использовать возможности языка запросов 1С по расчету итоговых полей.

Чтобы реквизиты шапки документа не принимали значение NULL на уровне группировок, мы можем принудительно рассчитать их значения с помощью агрегатных функций МАКСИМУМ или МИНИМУМ в секции ИТОГИ. Поскольку для одной конкретной ссылки документа ответственный и номер группы всегда уникальны, функции вернут их реальные значения.

Проанализируем пример такого запроса:


ВЫБРАТЬ
    ГруппыКурсанты.Курсанты.Наименование КАК Курсанты,
    ГруппыКурсанты.Курсанты.МестоРаботыКурсанта.Наименование КАК Филиал,
    ГруппыКурсанты.Ссылка.Ответственный КАК Ответственный,
    ГруппыКурсанты.Ссылка.НомерГруппы КАК НомерГруппы
ИЗ
    Документ.Группы.Курсанты КАК ГруппыКурсанты
ГДЕ
    ГруппыКурсанты.Ссылка В (&Ссылка)
УПОРЯДОЧИТЬ ПО
    Филиал,
    Курсанты
ИТОГИ 
    МАКСИМУМ(Ответственный), 
    МАКСИМУМ(НомерГруппы) 
ПО 
    Филиал

При таком объявлении итогов поля Ответственный и НомерГруппы будут доступны на уровне выборки группировок ВыборкаФилиалы. Программный обход в этом случае будет выглядеть так:


ВыборкаФилиалы = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);

ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ОбластьФилиалы = Макет.ПолучитьОбласть("Курсанты");
ОбластьСписок = ...

// Чтобы вывести заголовок, нам нужно спозиционироваться на первой записи группировки
Если ВыборкаФилиалы.Следующий() Тогда
    // Заполняем параметры заголовка (значения МАКСИМУМ(НомерГруппы) уже доступны здесь)
    ОбластьЗаголовок.Параметры.Заполнить(ВыборкаФилиалы);
    ТабДок.Вывести(ОбластьЗаголовок);
    
    // Возвращаем указатель в начало, чтобы не пропустить первый филиал в основном цикле
    ВыборкаФилиалы.Сбросить();
КонецЕсли;

Пока ВыборкаФилиалы.Следующий() Цикл
    // Стандартный вывод группировок по филиалам и детальных записей
    ...
КонецЦикла;

Обратите внимание на метод Сбросить(). Он необходим для того, чтобы вернуть указатель выборки в первоначальное состояние перед запуском основного цикла Пока, иначе первая группировка будет утеряна при выводе.

Способ 3. Оптимизация связи табличной части и шапки

В исходном запросе автора темы использовалось явное левое соединение табличной части с самим документом:


ИЗ
    Документ.Группы.Курсанты КАК ГруппыКурсанты
    ЛЕВОЕ СОЕДИНЕНИЕ Документ.Группы КАК Группы
    ПО ГруппыКурсанты.Ссылка = Группы.Ссылка

В платформе 1С:Предприятие это является классическим антипаттерном проектирования запросов. Каждая табличная часть документа по умолчанию содержит поле Ссылка, которое является системным полем связи с документом-владельцем.

Мы можем обращаться к любым реквизитам шапки документа напрямую «через точку» от поля Ссылка:

Платформа автоматически преобразует эти обращения в оптимальное соединение (SQL JOIN) на уровне СУБД, а текст запроса станет значительно короче, понятнее и будет защищен от случайных ошибок дублирования записей при левых соединениях.

Способ 4. Использование Дерева значений как альтернатива выборкам

При обходе иерархических структур данных последовательные выборки типа Выбрать().Выбрать() могут вызывать трудности при отладке и чтении кода. Хорошей альтернативой является выгрузка результатов запроса в объект типа ДеревоЗначений.

Давайте посмотрим, как можно элегантно решить эту задачу с помощью дерева:


// Выгружаем результат запроса сразу в дерево значений
ДеревоРезультата = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);

// Выводим шапку
Если ДеревоРезультата.Строки.Количество() > 0 Тогда
    // Берем первую строку первого уровня группировки
    ПерваяСтрокаГруппировки = ДеревоРезультата.Строки[0];
    
    // Если мы использовали МАКСИМУМ в Итогах, данные будут прямо в строке
    ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
    ОбластьЗаголовок.Параметры.Заполнить(ПерваяСтрокаГруппировки);
    ТабДок.Вывести(ОбластьЗаголовок);
КонецЕсли;

// Обходим дерево значений по строкам группировок (Филиалы)
Для Каждого СтрокаФилиал Из ДеревоРезультата.Строки Цикл
    
    ОбластьФилиалы.Параметры.Заполнить(СтрокаФилиал);
    ТабДок.Вывести(ОбластьФилиалы);
    
    СтрокаКурсантов = "";
    // Обходим подчиненные строки детальных записей (Курсанты)
    Для Каждого СтрокаДетали Из СтрокаФилиал.Строки Цикл
        СтрокаКурсантов = СтрокаКурсантов + СтрокаДетали.Курсанты + ", ";
    КонецЦикла;
    
    // Вывод списка курсантов...
    ...
КонецЦикла;

Работа с ДеревомЗначений делает структуру данных осязаемой. Вы всегда можете поставить точку останова в конфигураторе и детально изучить состав колонок и строк дерева в табло отладки, что существенно ускоряет локализацию ошибок.

Резюме

Подведем итоги. Если при обходе результата запроса по группировкам реквизиты шапки принимают значение NULL, используйте одно из проверенных решений:

← На главную