В процессе разработки в среде 1С:Предприятие начинающие специалисты часто сталкиваются со странным поведением системы при поиске элементов справочников. Типичный сценарий: код отлично работает на тестовой базе, но при переносе в рабочую среду или при создании реальных документов начинает выдавать непредсказуемые результаты или возвращать Неопределено. Разберем одну из таких классических ошибок, связанных с неверным пониманием разницы между ссылочными типами данных и их строковыми представлениями, а также рассмотрим, как правильно организовать проверку категорий объектов без лишней нагрузки на систему.
Рассмотрим практическую задачу. В договоре контрагента предусмотрено дополнительное свойство (категория) «Предоплата». Если у договора установлена эта категория (в интерфейсе это выглядит как галочка), то при печати счета на оплату в область дополнительной информации должен выводиться период, смещенный на один месяц вперед относительно даты документа. Если категория не установлена — выводится текущий месяц документа.
Для решения этой задачи был написан следующий первоначальный код:
Функция ПолучитьПредоплату(Контрагент1)
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КатегорииОбъектов.Объект КАК Объект,
| КатегорииОбъектов.Категория
|ИЗ
| РегистрСведений.КатегорииОбъектов КАК КатегорииОбъектов
|ГДЕ
| КатегорииОбъектов.Категория = &Предоплата
| И КатегорииОбъектов.Объект = &Контрагент";
Запрос.УстановитьПараметр("Предоплата", Справочники.КатегорииОбъектов.Предоплата);
// Попытка найти договор по наименованию:
Запрос.УстановитьПараметр("Контрагент", Справочники.ДоговорыКонтрагентов.НайтиПоНаименованию(Контрагент1));
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Если ВыборкаДетальныеЗаписи.Следующий() Тогда
Возврат ВыборкаДетальныеЗаписи.Объект;
КонецЕсли;
КонецФункции
Далее в процедуре печати логика обрабатывалась следующим образом:
ОбластьМакета = Макет.ПолучитьОбласть("Дополнение");
Если ПолучитьПредоплату(ПараметрыПечати.Договор) <> Неопределено Тогда
ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 1)), "ДЛФ=D");
ОбластьМакета.Параметры.ПоДату = Формат(КонецМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 1)), "ДЛФ=D");
Иначе
ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 0)), "ДЛФ=D");
ОбластьМакета.Параметры.ПоДату = Формат(КонецМесяца(ДобавитьМесяц(Объект.ДатаДокументов, 0)), "ДЛФ=D");
КонецЕсли;
Симптомы сбоя: если у контрагента заведено два договора с одинаковым наименованием (например, два договора с названием «Основной»), функция возвращает Неопределено, даже если у одного из них действительно установлена галочка «Предоплата». Но стоит переименовать один из договоров в «Основной1», как алгоритм тут же начинает работать корректно. Выясним причину этого странного поведения.
Проанализируем детально, почему этот код работает нестабильно и нарушает стандарты разработки платформы 1С:Предприятие 8.
Первая ошибка: путаница между Ссылкой и Наименованием. В процедуру печати передается параметр ПараметрыПечати.Договор. Этот параметр уже является готовым объектом типа СправочникСсылка.ДоговорыКонтрагентов. Передавая его в функцию ПолучитьПредоплату(Контрагент1) под именем Контрагент1, разработчик совершает подмену понятий. Вместо использования готового уникального идентификатора (ссылки), программа пытается выполнить поиск: Справочники.ДоговорыКонтрагентов.НайтиПоНаименованию(Контрагент1).
Поскольку в качестве аргумента передана ссылка, а не строка, платформа 1С осуществляет неявное преобразование типа — она получает строковое представление ссылки (то есть наименование договора, например, «Основной») и производит поиск по этой строке в базе данных.
Вторая ошибка: особенности работы метода НайтиПоНаименованию(). Справочник «Договоры контрагентов» является подчиненным по отношению к справочнику «Контрагенты». Более того, наименования договоров очень часто дублируются (у сотен контрагентов могут быть договоры с именем «Основной»). Метод НайтиПоНаименованию() без указания владельца выполняет поиск по всей таблице базы данных и возвращает первую попавшуюся запись, соответствующую строке поиска.
Когда у контрагента в базе присутствуют два договора «Основной»:
Метод НайтиПоНаименованию("Основной") возвращает ссылку на первый попавшийся договор (без категории). Запрос проверяет наличие записи в регистре сведений для этого договора, не находит её и возвращает Неопределено. Как только один из договоров переименовывается в «Основной1», его наименование становится уникальным, метод поиска находит именно его, и алгоритм срабатывает. Но полагаться на ручное переименование пользователями в реальной базе категорически нельзя.
Для исправления ситуации нам необходимо полностью отказаться от строкового поиска и переписать логику проверки. Ссылка на объект (UUID) уникальна в пределах всей базы данных, и СУБД выполняет выборку по ней мгновенно.
Уберем лишние операции конвертации типов и поиска. Напишем чистую функцию, которая принимает на вход ссылку на договор и проверяет наличие записи в регистре сведений. Избавимся от возврата объекта и заменим его на возвращение булева значения (Истина или Ложь).
Функция ЕстьПредоплатаПоДоговору(ДоговорСсылка) Экспорт
// Проверим корректность переданного значения
Если НЕ ЗначениеЗаполнено(ДоговорСсылка) Тогда
Возврат Ложь;
КонецЕсли;
Запрос = Новый Запрос;
// Оптимизируем текст запроса: выбираем только 1, чтобы не тратить ресурсы СУБД на чтение полей
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| 1 КАК Поле
|ИЗ
| РегистрСведений.КатегорииОбъектов КАК КатегорииОбъектов
|ГДЕ
| КатегорииОбъектов.Категория = &Предоплата
| И КатегорииОбъектов.Объект = &Договор";
Запрос.УстановитьПараметр("Предоплата", Справочники.КатегорииОбъектов.Предоплата);
Запрос.УстановитьПараметр("Договор", ДоговорСсылка);
РезультатЗапроса = Запрос.Выполнить();
// Быстрая проверка на пустоту результата запроса без обхода выборки
Возврат НЕ РезультатЗапроса.Пустой();
КонецФункции
В первоначальном коде автора также содержалась неоптимальная конструкция ДобавитьМесяц(Объект.ДатаДокументов, 0). Прибавление нуля месяцев не несет никакой полезной нагрузки и лишь тратит процессорное время. Избавимся от этой конструкции.
Посмотрим на исправленный вариант вызова функции в модуле печати:
ОбластьМакета = Макет.ПолучитьОбласть("Дополнение");
// Используем нашу оптимизированную функцию
Если ЕстьПредоплатаПоДоговору(ПараметрыПечати.Договор) Тогда
// Если предоплата есть, сдвигаем период на +1 месяц
ДатаСмещения = ДобавитьМесяц(Объект.ДатаДокументов, 1);
ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(ДатаСмещения), "ДЛФ=D");
ОбластьМакета.Параметры.ПоДату = Формат(КонецМесяца(ДатаСмещения), "ДЛФ=D");
Иначе
// Если предоплаты нет, выводим текущий месяц документа
ОбластьМакета.Параметры.СДаты = Формат(НачалоМесяца(Объект.ДатаДокументов), "ДЛФ=D");
ОбластьМакета.Параметры.ПоДату = Formат(КонецМесяца(Объект.ДатаДокументов), "ДЛФ=D");
КонецЕсли;
Рассматриваемый в теме регистр сведений КатегорииОбъектов — это классический механизм классификации из старых конфигураций 1С (таких как *Управление торговлей 10.3*, *Управление производственным предприятием 1.3*, *Комплексная автоматизация 1.1*).
В этом механизме связь между объектом и его свойством (категорией) хранится в независимом регистре сведений. Наличие записи в регистре означает, что объекту присвоена данная категория (эквивалент значения Истина). Отсутствие записи эквивалентно значению Ложь. Наш оптимизированный запрос с использованием метода Пустой() идеально ложится на эту архитектуру.
Важное примечание для современных конфигураций: если вы работаете в современных конфигурациях на базе БСП (например, *ERP 2*, *Управление торговлей 11*, *Бухгалтерия предприятия 3.0*), механизм категорий полностью заменен подсистемой «Свойства». Для проверки дополнительных реквизитов и сведений в них не следует писать прямые запросы к регистрам. Вместо этого используются функции стандартных общих модулей, например:
УправлениеСвойствами.ЗначениеСвойства(Объект, Свойство)
Применение стандартных методов БСП гарантирует, что ваш код не сломается при очередном обновлении конфигурации платформы.
Чтобы избежать подобных багов в будущем, рекомендуем всегда придерживаться следующих правил:
НайтиПоНаименованию() параметр Владелец.ВЫБРАТЬ ПЕРВЫЕ 1 1 вместо выборки реальных тяжелых полей данных. Это снижает трафик между сервером приложений и сервером СУБД.