При разработке и оптимизации кода в среде «1С:Предприятие» программисты часто прибегают к рефакторингу — выносу повторяющихся фрагментов кода в отдельные процедуры или функции. Однако иногда работающий ранее алгоритм после оформления в отдельную функцию неожиданно перестает работать: новые элементы справочников не создаются, данные не записываются в базу данных, а система не выдает очевидных синтаксических ошибок. Рассмотрим подробнее одну из таких классических ситуаций, проанализируем причины ее возникновения и разберем по шагам, как правильно спроектировать надежный и отказоустойчивый код для поиска и создания элементов справочника.
Рассмотрим исходный фрагмент кода функции, который привел к сбою создания элемента справочника:
Функция ПолучитьПодразделениеСсылка(ПодразделениеИмя, Организация)
ПодразделениеСсылка = Справочники.Подразделения.НайтиПоНаименованию(ПодразделениеИмя, , , Организация);
Если ПодразделениеСсылка = Справочники.Подразделения.ПустаяСсылка() Тогда
Подразделение = Справочники.Подразделения.СоздатьЭлемент();
Подразделение.Владелец = Организация;
Подразделение.Наименование = ПодразделениеИмя;
Подразделение.Записать();
ПодразделениеСсылка = Подразделение.Ссылка;
КонецЕсли;
Возврат (ПодразделениеСсылка);
КонецФункции
На первый взгляд, код написан логично: сначала система пытается найти подразделение по наименованию в контексте определенной организации (владельца). Если элемент не найден (возвращена пустая ссылка), то вызывается метод СоздатьЭлемент(), заполняются обязательные реквизиты, объект записывается, и функция возвращает ссылку на созданный элемент. Тем не менее на практике этот код перестает работать, если функция объявлена внутри модуля формы. Выясним скрытую причину такого поведения системы.
Главная причина, по которой элемент справочника отказывается создаваться — это конфликт имен переменных. В модулях управляемых или обычных форм область видимости реквизитов формы шире, чем область локальных переменных процедур и функций. Если на форме (или в самом объекте конфигурации, связанном с формой) присутствует реквизит с именем Подразделение (например, имеющий тип Строка или СправочникСсылка.Подразделения), то внутри функций модуля формы происходят следующие процессы:
Подразделение = Справочники.Подразделения.СоздатьЭлемент(); платформа не объявляет новую локальную переменную. Вместо этого она обращается напрямую к реквизиту формы с именем Подразделение.СправочникОбъект.Подразделения в реквизит формы, тип которого может быть жестко ограничен (например, типом Строка).Подразделение.Владелец и Подразделение.Наименование, система работает не с чистым локальным объектом, а с реквизитом формы, значение которого могло исказиться или не обновиться из-за несоответствия типов. В результате запись объекта либо вообще не происходит, либо завершается неявной ошибкой, которая может замалчиваться платформой в зависимости от контекста выполнения.Для решения этой проблемы необходимо строго следовать стандартам разработки фирмы «1С». Для всех создаваемых в коде прикладных объектов всегда следует использовать специализированный суффикс «Объект». Переименование переменной Подразделение в ПодразделениеОбъект полностью изолирует локальные вычисления от реквизитов формы.
Проанализируем строку поиска элемента: Справочники.Подразделения.НайтиПоНаименованию(ПодразделениеИмя, , , Организация);. В данном вызове пропущен второй параметр метода — ТочноеСоответствие. По умолчанию этот параметр принимает значение Ложь.
Рассмотрим, чем это грозит на практике. Если параметр ТочноеСоответствие равен Ложь, система выполняет поиск по началу строки (подобно оператору ПОДОБНО "ИскомоеИмя%" в СУБД). Если в вашей базе данных уже существует подразделение с наименованием «Отдел продаж и маркетинга», а вы программно пытаетесь найти или создать подразделение «Отдел продаж», метод НайтиПоНаименованию() найдет более длинное наименование, вернет его ссылку, а условие создания нового элемента не выполнится. Новое подразделение «Отдел продаж» никогда не будет создано. Для точного поиска всегда явно передавайте значение Истина во второй параметр метода.
В исходном коде используется конструкция Если ПодразделениеСсылка = Справочники.Подразделения.ПустаяСсылка() Тогда. Хотя данный подход работоспособен, он считается устаревшим и менее надежным. Существует более лаконичная и универсальная системная функция ЗначениеЗаполнено().
Применение конструкции Если Не ЗначениеЗаполнено(ПодразделениеСсылка) Тогда предпочтительнее по нескольким причинам. Функция ЗначениеЗаполнено() корректно обрабатывает не только пустые ссылки конкретного типа справочника, но и значения типов Неопределено или Null, которые могут возникнуть при различных сбоях в базе данных или при получении данных запросом. Такой код легче читать и поддерживать.
Если функция ПолучитьПодразделениеСсылка объявлена в модуле управляемой формы без директивы компиляции или с директивой &НаСервере, она выполняется в контексте формы. Это означает, что при каждом вызове функции с клиента на сервер передаются все данные формы (так называемый «контекст формы»), что снижает общую производительность системы.
Поскольку данная функция выполняет исключительно серверные операции (поиск в базе данных и создание нового элемента) и не использует напрямую другие реквизиты формы, ее крайне рекомендуется объявлять с директивой &НаСервереБезКонтекста. При бесконтекстном вызове контекст формы на сервер не передается, а любые попытки случайно обратиться к реквизитам формы (например, к нашему проблемному реквизиту Подразделение) вызовут явную ошибку компиляции еще на этапе разработки, что сразу укажет программисту на конфликт имен.
Проанализировав все слабые места исходного алгоритма, разработаем надежный, быстрый и безопасный вариант функции, полностью соответствующий стандартам разработки «1С»:
&НаСервереБезКонтекста
Функция ПолучитьПодразделениеСсылка(Знач ПодразделениеИмя, Знач Организация)
// Инициализируем возвращаемое значение пустой ссылкой
ПодразделениеСсылка = Справочники.Подразделения.ПустаяСсылка();
// 1. Поиск элемента с обязательным указанием точного соответствия (второй параметр - Истина)
ПодразделениеСсылка = Справочники.Подразделения.НайтиПоНаименованию(ПодразделениеИмя, Истина, , Организация);
// 2. Универсальная и безопасная проверка на заполненность полученного значения
Если Не ЗначениеЗаполнено(ПодразделениеСсылка) Тогда
// 3. Создаем новый объект с использованием суффикса "Объект" для исключения конфликта имен
ПодразделениеОбъект = Справочники.Подразделения.СоздатьЭлемент();
ПодразделениеОбъект.Владелец = Организация;
ПодразделениеОбъект.Наименование = ПодразделениеИмя;
// 4. Безопасная запись объекта с обработкой возможных исключений
Попытка
ПодразделениеОбъект.Записать();
ПодразделениеСсылка = ПодразделениеОбъект.Ссылка;
Исключение
// В случае ошибки записи (например, блокировки базы или срабатывания подписок) фиксируем инцидент
ЗаписьЖурналаРегистрации(
"Справочники.Подразделения.СозданиеЭлемента",
УровеньЖурналаРегистрации.Ошибка,
,
,
ПодробноеПредставлениеОшибки(ИнформацияОбОшибке())
);
ВызватьИсключение "Не удалось создать новое подразделение. Обратитесь к администратору.";
КонецПопытки;
КонецЕсли;
Возврат ПодразделениеСсылка;
КонецФункции
Подведем итоги нашего рефакторинга. Использование директивы &НаСервереБезКонтекста существенно увеличило скорость работы функции и полностью изолировало код от влияния интерфейсных реквизитов. Четкое следование стандартам именования локальных переменных (использование ПодразделениеОбъект) гарантирует отсутствие скрытых конфликтов типов. Наконец, добавление конструкции Попытка...Исключение с логированием в журнал регистрации защищает систему от аварийного завершения работы пользователей в случае непредвиденных ошибок СУБД или наложения транзакционных блокировок.