Пустая ссылка в запросе 1С — частый источник тихих ошибок: нужно найти документы без контрагента или товары без цены, а запрос возвращает то пустой результат, то лишние строки. Обычно синтаксис ни при чём: условие сравнивает поле с пустой ссылкой, а в поле на самом деле лежит NULL или Неопределено. В статье разобрано условие для каждого из этих значений в платформе 8.3 (файловый и клиент-серверный вариант) и приведён полный пример проверки документов для отчёта или обработки.
Пустая ссылка — полноценное значение ссылочного типа (например, СправочникСсылка.Контрагенты), которое не указывает ни на один объект в базе. Именно она лежит в реквизите, если пользователь оставил поле «Контрагент» пустым и записал документ. Чтобы проверить в условии запроса пустую ссылку на справочник или документ, используют конструкцию ЗНАЧЕНИЕ():
ВЫБРАТЬ
Реализация.Ссылка КАК Документ,
Реализация.Дата КАК Дата
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
Реализация.Контрагент = ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)
И НЕ Реализация.ПометкаУдаления
Имена объектов в примерах условные — подставьте свои (в УТ 11, например, документ заказа называется ЗаказКлиента).
Внутри скобок указывается вид объекта в единственном числе (Справочник, Документ, Перечисление), имя объекта из дерева конфигурации и слово ПустаяСсылка — без скобок, это не вызов метода. Та же конструкция служит для предопределённых элементов и значений перечислений: ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.НДС20). Опечатка в имени всплывёт только при выполнении: для модуля текст запроса — просто строка. Проверить его заранее можно в конструкторе запроса: он сообщит о неверном имени.
Второй вариант — передать пустую ссылку параметром: так удобнее, когда значение выбирается в коде или текст запроса собирается программно.
&НаСервереБезКонтекста
Процедура НайтиДокументыБезДоговораНаСервере()
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК Документ
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Договор = &ПустойДоговор
| И НЕ Реализация.ПометкаУдаления";
Запрос.УстановитьПараметр("ПустойДоговор", Справочники.ДоговорыКонтрагентов.ПустаяСсылка());
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// В конфигурации на БСП 3.x вместо Сообщить() используйте
// ОбщегоНазначения.СообщитьПользователю().
Сообщить(СтрШаблон("Не указан договор: %1", Выборка.Документ));
КонецЦикла;
КонецПроцедуры
Процедура не обращается к данным формы — отсюда &НаСервереБезКонтекста: контекст формы на сервер не передаётся. СтрШаблон() есть с платформы 8.3.6; в более старых релизах или режиме совместимости замените её конкатенацией строк.
По скорости способы равноценны: СУБД сравнивает поле с конкретным значением и может использовать индекс. Зато запрос с ЗНАЧЕНИЕ() самодостаточен — его можно выполнить в консоли без подготовки параметров.
Синтаксис одинаков для всех ссылочных типов. Правило: в тексте запроса — единственное число, в коде — менеджер во множественном числе и вызов метода.
| Вид объекта | В тексте запроса | В коде (для параметра) |
|---|---|---|
| Справочник | ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка) | Справочники.Контрагенты.ПустаяСсылка() |
| Документ | ЗНАЧЕНИЕ(Документ.ЗаказПокупателя.ПустаяСсылка) | Документы.ЗаказПокупателя.ПустаяСсылка() |
| Перечисление | ЗНАЧЕНИЕ(Перечисление.СтавкиНДС.ПустаяСсылка) | Перечисления.СтавкиНДС.ПустаяСсылка() |
| План видов характеристик | ЗНАЧЕНИЕ(ПланВидовХарактеристик.ВидыСубконто.ПустаяСсылка) | ПланыВидовХарактеристик.ВидыСубконто.ПустаяСсылка() |
| План счетов | ЗНАЧЕНИЕ(ПланСчетов.Хозрасчетный.ПустаяСсылка) | ПланыСчетов.Хозрасчетный.ПустаяСсылка() |
На клиенте менеджеров объектов нет, и в коде формы пустую ссылку получают функцией ПредопределенноеЗначение() по полному имени:
&НаКлиенте
Процедура ОчиститьЗаказ(Команда)
// Менеджеры объектов на клиенте недоступны, поэтому пустую ссылку
// получаем по полному имени.
Объект.Заказ = ПредопределенноеЗначение("Документ.ЗаказПокупателя.ПустаяСсылка");
КонецПроцедуры
NULL — не «пустое значение», а отсутствие значения. В реквизите записанного объекта его, как правило, нет: там лежит ссылка, пустая ссылка или (для составного типа) Неопределено. Исключение — иерархические справочники: реквизит с использованием «Для элемента» у групп (и «Для группы» у элементов) в запросе возвращает NULL. Условие Номенклатура.ЕдиницаИзмерения = ЗНАЧЕНИЕ(Справочник.ЕдиницыИзмерения.ПустаяСсылка) группы не найдёт. Кроме того, NULL рождается внутри запроса:
Реализация.Контрагент.ИНН для документа без контрагента вернёт NULL (платформа строит здесь левое соединение);ВЫРАЗИТЬ(), если в поле лежит значение другого типа;СУММА, МАКСИМУМ) над пустым набором строк и в ВЫБОР без ветки ИНАЧЕ, если ни одно условие не выполнилось.Главное следствие: пустая ссылка и NULL не равны друг другу, и строки с NULL сравнением с пустой ссылкой не найти.
В клиент-серверном варианте запрос 1С транслируется в SQL, а файловая СУБД реализует ту же логику: любое сравнение с NULL (=, <>, >) даёт не ИСТИНА и не ЛОЖЬ, а «неизвестно». Секция ГДЕ пропускает только строки, для которых условие истинно, так что ГДЕ Цены.Цена = NULL не вернёт ни одной строки. Для проверки существует отдельный оператор: Поле ЕСТЬ NULL, а отрицание записывается как Поле ЕСТЬ НЕ NULL или НЕ Поле ЕСТЬ NULL.
Строки с NULL не проходят и условия с неравенством: ГДЕ Цены.Цена <> 0 отбросит не только нулевые цены, но и товары вообще без записи в регистре.
Оператор ЕСТЬ NULL проверяет, что значения нет. Применённый к полю из условия соединения, он отвечает на вопрос «нашлась ли пара в присоединённой таблице». Функция ЕСТЬNULL(Выражение, Замена) возвращает замену, если выражение равно NULL, а иначе само выражение. Её используют, чтобы привести NULL к нормальному значению: к нулю для чисел, к пустой ссылке для ссылок. Пример — товары, у которых цены нет совсем или она нулевая:
ВЫБРАТЬ
Номенклатура.Ссылка КАК Номенклатура,
ВЫБОР
КОГДА Цены.Номенклатура ЕСТЬ NULL
ТОГДА "Цена не установлена"
ИНАЧЕ "Цена равна нулю"
КОНЕЦ КАК Причина
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Дата, ТипЦен = &ТипЦен) КАК Цены
ПО Номенклатура.Ссылка = Цены.Номенклатура
ГДЕ
НЕ Номенклатура.ЭтоГруппа
И ЕСТЬNULL(Цены.Цена, 0) = 0
Наличие пары проверяем по полю из условия соединения — Цены.Номенклатура. Если бы в ГДЕ стояло только Цены.Цена = 0, товары без записи в регистре в результат не попали бы.
Та же функция помогает сравнить NULL с пустой ссылкой: условие ЕСТЬNULL(Реализация.Контрагент.ГоловнойКонтрагент, ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)) = ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка) истинно и когда контрагент не указан, и когда у него не заполнен головной контрагент. Но функция над полем мешает СУБД использовать индекс: на больших таблицах добавьте отбор по периоду или другому индексированному полю.
С составным типом сложнее. Пока пользователь не выбрал тип значения, в реквизите лежит Неопределено. Если тип выбран, а значение нет — пустая ссылка этого типа. Значит, «реквизит не заполнен» для составного типа — это сразу несколько условий:
ВЫБРАТЬ
Реализация.Ссылка КАК Документ
ИЗ
Документ.РеализацияТоваровУслуг КАК Реализация
ГДЕ
(Реализация.Основание = НЕОПРЕДЕЛЕНО
ИЛИ Реализация.Основание = ЗНАЧЕНИЕ(Документ.ЗаказПокупателя.ПустаяСсылка)
ИЛИ Реализация.Основание = ЗНАЧЕНИЕ(Документ.СчетНаОплатуПокупателю.ПустаяСсылка))
Аналога ЗначениеЗаполнено() в языке запросов нет — варианты приходится перечислять явно. Если типов много или состав типа меняется при обновлениях, надёжнее собрать массив пустых значений в коде по метаданным и передать его параметром, как в полном примере ниже. Он учитывает и примитивные типы: если в составной тип входит Строка, Число или Дата, незаполненными будут и "", 0, пустая дата. А для поля из левого соединения добавьте ещё ЕСТЬ NULL.
У приёма два ограничения. Строки неограниченной длины в условиях запроса сравнивать нельзя: если такая строка входит в составной тип, задайте ей фиксированную длину или проверяйте поле в коде после запроса. А для ЛюбаяСсылка и широких определяемых типов массив разрастётся до сотен значений — там проще явно проверить Неопределено и нужные типы.
Чтобы проверить не заполненность, а тип значения, используйте оператор ССЫЛКА: Реализация.Основание ССЫЛКА Документ.ЗаказПокупателя. Он проверяет только тип, поэтому истинен и для пустой ссылки на заказ.
Функция для серверного общего модуля собирает всё сказанное в пакетный запрос. Она находит проведённые реализации за период, у которых не указан контрагент или заказ, не заполнено основание составного типа или нет движений по регистру продаж.
// Серверный общий модуль (или модуль объекта отчета/обработки).
// Имена объектов метаданных условные: замените их на имена из своей конфигурации.
// Находит проведенные реализации за период, в которых:
// - не указан контрагент (пустая ссылка на справочник);
// - не указан заказ (пустая ссылка на документ);
// - не заполнено основание (реквизит составного типа);
// - нет ни одной записи в регистре накопления Продажи (NULL после левого соединения).
//
// Параметры:
// ДатаНачала - Дата - начало периода.
// ДатаОкончания - Дата - конец периода.
//
// Возвращаемое значение:
// ТаблицаЗначений - колонки Документ, Дата, НетКонтрагента, НетЗаказа, НетОснования, НетДвижений.
//
Функция РеализацииСНезаполненнымиДанными(Знач ДатаНачала, Знач ДатаОкончания) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Реализация.Ссылка КАК Ссылка,
| Реализация.Дата КАК Дата,
| Реализация.Контрагент КАК Контрагент,
| Реализация.Заказ КАК Заказ,
| Реализация.Основание КАК Основание
|ПОМЕСТИТЬ ВТ_Документы
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Реализация
|ГДЕ
| Реализация.Дата МЕЖДУ &ДатаНачала И &ДатаОкончания
| И Реализация.Проведен
|
|ИНДЕКСИРОВАТЬ ПО
| Ссылка
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| Продажи.Регистратор КАК Регистратор
|ПОМЕСТИТЬ ВТ_Движения
|ИЗ
| РегистрНакопления.Продажи КАК Продажи
|ГДЕ
| Продажи.Регистратор В
| (ВЫБРАТЬ
| ВТ_Документы.Ссылка
| ИЗ
| ВТ_Документы КАК ВТ_Документы)
|
|ИНДЕКСИРОВАТЬ ПО
| Регистратор
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Реализация.Ссылка КАК Документ,
| Реализация.Дата КАК Дата,
| ВЫБОР
| КОГДА Реализация.Контрагент = ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК НетКонтрагента,
| ВЫБОР
| КОГДА Реализация.Заказ = ЗНАЧЕНИЕ(Документ.ЗаказПокупателя.ПустаяСсылка)
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК НетЗаказа,
| ВЫБОР
| КОГДА Реализация.Основание = НЕОПРЕДЕЛЕНО
| ИЛИ Реализация.Основание В (&ПустыеЗначенияОснования)
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК НетОснования,
| ВЫБОР
| КОГДА Движения.Регистратор ЕСТЬ NULL
| ТОГДА ИСТИНА
| ИНАЧЕ ЛОЖЬ
| КОНЕЦ КАК НетДвижений
|ИЗ
| ВТ_Документы КАК Реализация
| ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Движения КАК Движения
| ПО Реализация.Ссылка = Движения.Регистратор
|ГДЕ
| Реализация.Контрагент = ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)
| ИЛИ Реализация.Заказ = ЗНАЧЕНИЕ(Документ.ЗаказПокупателя.ПустаяСсылка)
| ИЛИ Реализация.Основание = НЕОПРЕДЕЛЕНО
| ИЛИ Реализация.Основание В (&ПустыеЗначенияОснования)
| ИЛИ Движения.Регистратор ЕСТЬ NULL
|
|УПОРЯДОЧИТЬ ПО
| Дата";
Запрос.УстановитьПараметр("ДатаНачала", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДатаОкончания", КонецДня(ДатаОкончания));
Запрос.УстановитьПараметр("ПустыеЗначенияОснования", ПустыеЗначенияСоставногоТипа(
Метаданные.Документы.РеализацияТоваровУслуг.Реквизиты.Основание.Тип));
Возврат Запрос.Выполнить().Выгрузить();
КонецФункции
// Возвращает массив "пустых" значений для каждого типа из описания типов:
// пустую ссылку для ссылочного типа, "" для Строки, 0 для Числа, пустую дату для Даты.
// Булево пропускается: Ложь - это осмысленное значение, а не "не заполнено".
//
// Ограничения: функция рассчитана на составной тип из нескольких конкретных типов.
// Для ЛюбаяСсылка и широких определяемых типов массив получится из сотен значений,
// в таком случае проверяйте Неопределено и нужные типы явно. Строку неограниченной
// длины в запросе нельзя сравнить через = или В, поэтому она тоже пропускается:
// такой реквизит проверяйте в коде после выполнения запроса.
//
// Параметры:
// ОписаниеТиповРеквизита - ОписаниеТипов - тип реквизита из метаданных.
//
// Возвращаемое значение:
// Массив - пустые значения допустимых типов, кроме Булево и Строки неограниченной длины.
//
Функция ПустыеЗначенияСоставногоТипа(ОписаниеТиповРеквизита)
Результат = Новый Массив;
Для Каждого ТекущийТип Из ОписаниеТиповРеквизита.Типы() Цикл
Если ТекущийТип = Тип("Булево") Тогда
Продолжить;
КонецЕсли;
Если ТекущийТип = Тип("Строка")
И ОписаниеТиповРеквизита.КвалификаторыСтроки.Длина = 0 Тогда
Продолжить;
КонецЕсли;
МассивТипов = Новый Массив;
МассивТипов.Добавить(ТекущийТип);
// Описание из одного типа приводит Неопределено к значению по умолчанию
// этого типа: для ссылки - пустая ссылка, для Строки/Числа/Даты - "", 0, пустая дата.
ОписаниеОдногоТипа = Новый ОписаниеТипов(МассивТипов);
Результат.Добавить(ОписаниеОдногоТипа.ПривестиЗначение(Неопределено));
КонецЦикла;
Возврат Результат;
КонецФункции
Регистратор. Отбирать движения по Продажи.Период нельзя, потому что период задаёт код проведения и он не обязан совпадать с датой документа. Реализация, чьи движения записаны другой датой, ложно попала бы в «без движений», а во временную таблицу попали бы лишние регистраторы, например возвраты. Итоговый запрос берёт документы из ВТ_Документы, так что отбор по периоду задан в одном месте.Движения.Регистратор будет NULL — это и проверяет ЕСТЬ NULL.ЗНАЧЕНИЕ(...ПустаяСсылка). Основание — составного типа: проверяем и «тип не выбран», и пустое значение любого допустимого типа.ПривестиЗначение(Неопределено) возвращает значение по умолчанию: пустую ссылку, пустую строку, 0 или пустую дату. Булево пропускаем: Ложь — осмысленное значение. Строку неограниченной длины тоже: в запросе её не сравнить.Поле ЕСТЬ NULL.ЕСТЬ NULL или оберните поле в ЕСТЬNULL().ЗНАЧЕНИЕ(Справочники.Контрагенты.ПустаяСсылка), ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка()) или имя несуществующего объекта — запрос не скомпилируется. Платформа сообщит об ошибке в тексте запроса и укажет строку и позицию в фигурных скобках, например {(7, 32)}.&ПустойДоговор, а УстановитьПараметр() для него не вызван, выполнение прервётся с ошибкой о незаданном значении параметра. Имя в тексте и в коде должно совпадать до буквы.Неопределено и пустые значения других типов.Цены.ТипЦен = &ТипЦен) отсекает строки с NULL, и соединение фактически становится внутренним. Такой отбор ставьте в условие ПО или в параметры виртуальной таблицы. Исключение — ЕСТЬ NULL и ЕСТЬNULL(): их в ГДЕ ставят как раз для поиска строк без пары.Нет. В тексте запроса незаполненность проверяется сравнением с пустой ссылкой, НЕОПРЕДЕЛЕНО и ЕСТЬ NULL. В коде, после выполнения запроса, проверяют функцией ЗначениеЗаполнено(): она возвращает Ложь и для пустой ссылки, и для NULL. Есть и метод ссылки Пустая(), но если в поле пришёл NULL, его вызов даст ошибку.
Условием Поле <> ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка). Строки с NULL такое условие тоже не пропустит, исключать их отдельно не нужно. Для составного типа — НЕ (Поле = НЕОПРЕДЕЛЕНО ИЛИ Поле В (&ПустыеЗначения)).
Функцией ЕСТЬNULL(Поле, ЗНАЧЕНИЕ(Справочник.Контрагенты.ПустаяСсылка)) в секции ВЫБРАТЬ. Это полезно, когда результат дальше обрабатывает код, который не ожидает NULL.
Да. Текст набора данных СКД и запрос динамического списка пишутся на том же языке запросов, так что ЗНАЧЕНИЕ(), ЕСТЬ NULL и ЕСТЬNULL() в них работают так же. Параметры задаются через настройки компоновки или параметры списка.
Сравнением с ДАТАВРЕМЯ(1, 1, 1): ГДЕ Реализация.СрокОплаты = ДАТАВРЕМЯ(1, 1, 1). Незаполненная дата — это конкретное значение 01.01.0001 00:00:00, а не NULL, и ЕСТЬ NULL её не найдёт.
Если вместо одного условия нужен полноценный контроль — отчёт на СКД, который ищет незаполненные документы по всей базе, или проверка, не дающая провести документ без обязательных реквизитов, — такую задачу быстрее отдать программисту. На 1c-only.ru можно заказать разработку отчётов и обработок 1С или просто разместить задачу: оценка бесплатная, ответ — в течение рабочего дня.
Станьте частью сообщества!
Войдите или зарегистрируйтесь, и вы сможете участвовать в обсуждениях.