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