Разработка печатных форм и отчетов — одна из самых частых задач программиста 1С. Однако при работе с запросами, содержащими итоги и группировки, разработчики регулярно сталкиваются с неприятным поведением платформы: реквизиты шапки документа (например, НомерГруппы, Ответственный, Организация) выводятся пустыми или принимают значение NULL. При этом в консоли запросов данные могут отображаться корректно.
В этой статье мы подробно проанализируем, почему возникает эта проблема, разберем внутренние механизмы работы выборок 1С и рассмотрим несколько надежных способов решения задачи в соответствии со стандартами разработки фирмы «1С».
Рассмотрим классическую ситуацию. У нас есть документ Группы, у которого есть табличная часть Курсанты. Нам нужно вывести печатную форму, сгруппированную по филиалам (месту работы курсантов), а в шапке печатной формы отобразить общие реквизиты документа: НомерГруппы и Ответственный.
В исходном (проблемном) коде запрос выглядел следующим образом:
ВЫБРАТЬ
ГруппыКурсанты.Курсанты.Наименование КАК Курсанты,
ГруппыКурсанты.Курсанты.МестоРаботыКурсанта.Наименование КАК Филиал,
Группы.Ответственный,
Группы.НомерГруппы
ИЗ
Документ.Группы.Курсанты КАК ГруппыКурсанты
ЛЕВОЕ СОЕДИНЕНИЕ Документ.Группы КАК Группы
ПО ГруппыКурсанты.Ссылка = Группы.Ссылка
ГДЕ
Группы.Ссылка В (&Ссылка)
УПОРЯДОЧИТЬ ПО
Филиал,
Курсанты
ИТОГИ ПО
Филиал
При обходе результата этого запроса с помощью конструкции Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам) разработчик пытается заполнить параметры шапки до начала обхода детальных записей:
ВыборкаФилиалы = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
// Попытка заполнить параметры шапки
ОбластьЗаголовок.Параметры.Заполнить(ВыборкаФилиалы);
ТабДок.Вывести(ОбластьЗаголовок);
Здесь кроются сразу две серьезные ошибки:
ВыборкаИзРезультатаЗапроса указатель находится в позиции «перед первой записью». В этот момент выборка не содержит никаких данных. Любая попытка прочитать свойства или вызвать метод Заполнить() приведет к тому, что параметры макета останутся пустыми. Чтобы получить доступ к данным первого уровня группировки, необходимо хотя бы один раз вызвать ВыборкаФилиалы.Следующий().ИТОГИ ПО Филиал, платформа строит иерархическую структуру данных. На верхнем уровне (уровень группировки Филиал) заполняются только те поля, которые указаны в секции ПО (в нашем случае это Филиал), и поля, к которым применены агрегатные функции (например, СУММА, КОЛИЧЕСТВО) в секции ИТОГИ. Все остальные поля детальных записей, включая реквизиты шапки документа Ответственный и НомерГруппы, на уровне этой группировки физически равны NULL. Они появляются только тогда, когда мы спускаемся на уровень ниже — к детальным записям (курсантам).Рассмотрим подробнее четыре способа исправления этой архитектурной проблемы.
По стандартам разработки фирмы «1С», используемым в Библиотеке стандартных подсистем (БСП), смешивать получение данных шапки и табличной части в одном запросе с итогами не рекомендуется. Гораздо эффективнее и чище разделить получение данных на два независимых шага в рамках одного пакета запросов.
Разберем этот вариант по шагам. Первый запрос пакета будет выбирать только реквизиты шапки документа по ссылке. Второй запрос пакета выберет детальные записи табличной части с группировкой по филиалам.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Группы.Ответственный КАК Ответственный,
| Группы.НомерГруппы КАК НомерГруппы
|ИЗ
| Документ.Группы КАК Группы
|ГДЕ
| Группы.Ссылка = &Ссылка
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ГруппыКурсанты.Курсанты.Наименование КАК Курсанты,
| ГруппыКурсанты.Курсанты.МестоРаботыКурсанта.Наименование КАК Филиал
|ИЗ
| Документ.Группы.Курсанты КАК ГруппыКурсанты
|ГДЕ
| ГруппыКурсанты.Ссылка = &Ссылка
|УПОРЯДОЧИТЬ ПО
| Филиал,
| Курсанты
|ИТОГИ ПО
| Филиал";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
// Выполняем пакет запросов
РезультатПакета = Запрос.ВыполнитьПакет();
// 1. Выводим заголовок из первого запроса пакета
ВыборкаШапка = РезультатПакета[0].Выбрать();
Если ВыборкаШапка.Следующий() Тогда
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ОбластьЗаголовок.Параметры.Заполнить(ВыборкаШапка);
ТабДок.Вывести(ОбластьЗаголовок);
КонецЕсли;
// 2. Выводим группировки и детальные записи из второго запроса пакета
ВыборкаФилиалы = РезультатПакета[1].Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
ОбластьФилиалы = Макет.ПолучитьОбласть("Курсанты");
ОбластьСписок = Макет.ПолучитьОбласть("Список");
ОбластьПодвал = Макет.ПолучитьОбласть("Низ");
Пока ВыборкаФилиалы.Следующий() Цикл
// Выводим группировку по Филиалу
ОбластьФилиалы.Параметры.Заполнить(ВыборкаФилиалы);
ТабДок.Вывести(ОбластьФилиалы);
СтрокаКурсантов = "";
// Получаем детальные записи по конкретному филиалу
ВыборкаДетали = ВыборкаФилиалы.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
СтрокаКурсантов = СтрокаКурсантов + ВыборкаДетали.Курсанты + ", ";
КонецЦикла;
// Форматируем строку и выводим список курсантов
Если ЗначениеЗаполнено(СтрокаКурсантов) Тогда
СтрокаКурсантов = Лев(СтрокаКурсантов, СтрДлина(СтрокаКурсантов) - 2) + ".";
КонецЕсли;
ОбластьСписок.Параметры.Курсанты = СтрокаКурсантов;
ТабДок.Вывести(ОбластьСписок);
КонецЦикла;
// Выводим подвал печатной формы
ОбластьПодвал.Параметры.Заполнить(ВыборкаШапка); // Параметры подвала (например, Ответственный) берем из шапки
ТабДок.Вывести(ОбластьПодвал);
Преимущества данного подхода очевидны: мы избавляемся от лишних значений NULL, код становится линейным, простым для понимания и отладки, а производительность запроса возрастает за счет отсутствия избыточных соединений.
Если вам по какой-то причине необходимо решить задачу строго в рамках одного запроса, можно использовать возможности языка запросов 1С по расчету итоговых полей.
Чтобы реквизиты шапки документа не принимали значение NULL на уровне группировок, мы можем принудительно рассчитать их значения с помощью агрегатных функций МАКСИМУМ или МИНИМУМ в секции ИТОГИ. Поскольку для одной конкретной ссылки документа ответственный и номер группы всегда уникальны, функции вернут их реальные значения.
Проанализируем пример такого запроса:
ВЫБРАТЬ
ГруппыКурсанты.Курсанты.Наименование КАК Курсанты,
ГруппыКурсанты.Курсанты.МестоРаботыКурсанта.Наименование КАК Филиал,
ГруппыКурсанты.Ссылка.Ответственный КАК Ответственный,
ГруппыКурсанты.Ссылка.НомерГруппы КАК НомерГруппы
ИЗ
Документ.Группы.Курсанты КАК ГруппыКурсанты
ГДЕ
ГруппыКурсанты.Ссылка В (&Ссылка)
УПОРЯДОЧИТЬ ПО
Филиал,
Курсанты
ИТОГИ
МАКСИМУМ(Ответственный),
МАКСИМУМ(НомерГруппы)
ПО
Филиал
При таком объявлении итогов поля Ответственный и НомерГруппы будут доступны на уровне выборки группировок ВыборкаФилиалы. Программный обход в этом случае будет выглядеть так:
ВыборкаФилиалы = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ОбластьФилиалы = Макет.ПолучитьОбласть("Курсанты");
ОбластьСписок = ...
// Чтобы вывести заголовок, нам нужно спозиционироваться на первой записи группировки
Если ВыборкаФилиалы.Следующий() Тогда
// Заполняем параметры заголовка (значения МАКСИМУМ(НомерГруппы) уже доступны здесь)
ОбластьЗаголовок.Параметры.Заполнить(ВыборкаФилиалы);
ТабДок.Вывести(ОбластьЗаголовок);
// Возвращаем указатель в начало, чтобы не пропустить первый филиал в основном цикле
ВыборкаФилиалы.Сбросить();
КонецЕсли;
Пока ВыборкаФилиалы.Следующий() Цикл
// Стандартный вывод группировок по филиалам и детальных записей
...
КонецЦикла;
Обратите внимание на метод Сбросить(). Он необходим для того, чтобы вернуть указатель выборки в первоначальное состояние перед запуском основного цикла Пока, иначе первая группировка будет утеряна при выводе.
В исходном запросе автора темы использовалось явное левое соединение табличной части с самим документом:
ИЗ
Документ.Группы.Курсанты КАК ГруппыКурсанты
ЛЕВОЕ СОЕДИНЕНИЕ Документ.Группы КАК Группы
ПО ГруппыКурсанты.Ссылка = Группы.Ссылка
В платформе 1С:Предприятие это является классическим антипаттерном проектирования запросов. Каждая табличная часть документа по умолчанию содержит поле Ссылка, которое является системным полем связи с документом-владельцем.
Мы можем обращаться к любым реквизитам шапки документа напрямую «через точку» от поля Ссылка:
ГруппыКурсанты.Ссылка.ОтветственныйГруппыКурсанты.Ссылка.НомерГруппыПлатформа автоматически преобразует эти обращения в оптимальное соединение (SQL JOIN) на уровне СУБД, а текст запроса станет значительно короче, понятнее и будет защищен от случайных ошибок дублирования записей при левых соединениях.
При обходе иерархических структур данных последовательные выборки типа Выбрать().Выбрать() могут вызывать трудности при отладке и чтении кода. Хорошей альтернативой является выгрузка результатов запроса в объект типа ДеревоЗначений.
Давайте посмотрим, как можно элегантно решить эту задачу с помощью дерева:
// Выгружаем результат запроса сразу в дерево значений
ДеревоРезультата = Запрос.Выполнить().Выгрузить(ОбходРезультатаЗапроса.ПоГруппировкам);
// Выводим шапку
Если ДеревоРезультата.Строки.Количество() > 0 Тогда
// Берем первую строку первого уровня группировки
ПерваяСтрокаГруппировки = ДеревоРезультата.Строки[0];
// Если мы использовали МАКСИМУМ в Итогах, данные будут прямо в строке
ОбластьЗаголовок = Макет.ПолучитьОбласть("Заголовок");
ОбластьЗаголовок.Параметры.Заполнить(ПерваяСтрокаГруппировки);
ТабДок.Вывести(ОбластьЗаголовок);
КонецЕсли;
// Обходим дерево значений по строкам группировок (Филиалы)
Для Каждого СтрокаФилиал Из ДеревоРезультата.Строки Цикл
ОбластьФилиалы.Параметры.Заполнить(СтрокаФилиал);
ТабДок.Вывести(ОбластьФилиалы);
СтрокаКурсантов = "";
// Обходим подчиненные строки детальных записей (Курсанты)
Для Каждого СтрокаДетали Из СтрокаФилиал.Строки Цикл
СтрокаКурсантов = СтрокаКурсантов + СтрокаДетали.Курсанты + ", ";
КонецЦикла;
// Вывод списка курсантов...
...
КонецЦикла;
Работа с ДеревомЗначений делает структуру данных осязаемой. Вы всегда можете поставить точку останова в конфигураторе и детально изучить состав колонок и строк дерева в табло отладки, что существенно ускоряет локализацию ошибок.
Подведем итоги. Если при обходе результата запроса по группировкам реквизиты шапки принимают значение NULL, используйте одно из проверенных решений:
Ссылка.ИмяРеквизита.