Каждый начинающий разработчик в среде «1С:Предприятие 8» рано или поздно сталкивается с задачами массового изменения или заполнения реквизитов в справочниках. Казалось бы, задача тривиальная: обойти элементы, проверить, заполнено ли поле, и, если нет, записать нужное значение. Однако на практике здесь кроется множество подводных камней, связанных со спецификой типов данных в 1С, работой с пустой ссылкой, значениями NULL, Неопределено и производительностью базы данных.
В этой статье мы подробно разберем, почему возникают популярные ошибки при работе со ссылочными типами, как правильно проверять незаполненные поля, корректно присваивать новые значения и как оптимизировать этот процесс с помощью запросов, чтобы ваша обработка не «повесила» рабочую базу данных.
Давайте проанализируем код, с которого началась дискуссия на форуме. Разработчик пытался заполнить реквизит БазоваяЕдиницаИзмерения в справочнике Номенклатура конкретным значением (кодом 796, что соответствует штукам в классификаторе ОКЕИ):
Номенклатура = Справочники.Номенклатура;
Выборка = Номенклатура.Выбрать();
Пока Выборка.Следующий() Цикл
Объект = Выборка.ПолучитьОбъект();
Если Объект.БазоваяЕдиницаИзмерения = NULL Тогда
Если Объект.ЭтоГруппа Тогда
Продолжить;
Иначе
Объект.БазоваяЕдиницаИзмерения = 796;
Объект.Записать();
КонецЕсли;
КонецЕсли;
КонецЦикла;
В этом небольшом фрагменте кода скрыто сразу несколько критических ошибок, которые приводят либо к неработоспособности кода, либо к падению производительности системы:
NULL: Встроенный язык 1С не поддерживает такое сравнение в обычном коде (вне запросов). Конструкция Объект.БазоваяЕдиницаИзмерения = NULL всегда будет возвращать Ложь, и код внутри условия просто никогда не выполнится.Объект.БазоваяЕдиницаИзмерения = 796 ошибочна. Реквизит БазоваяЕдиницаИзмерения имеет тип ссылки (например, СправочникСсылка.КлассификаторЕдиницИзмерения). Платформа не умеет автоматически преобразовывать число 796 в нужную ссылку. Ей необходимо передать именно объект ссылки.Выбрать() и последующее получение объекта через ПолучитьОбъект() для каждой записи — это тяжелая операция, которая заставляет СУБД считывать терабайты лишней информации и блокировать таблицы.Когда коллеги на форуме предложили заменить проверку на Объект.БазоваяЕдиницаИзмерения.Пустая(), автор столкнулся с ошибкой: «Значение не является значением объектного типа (Пустая)». Давайте разберем по шагам, почему так произошло.
В платформе 1С отсутствие значения в реквизите может выражаться тремя разными способами:
ПустаяСсылка). Это объект ссылочного типа, у которого уникальный идентификатор состоит из одних нулей. Метод Пустая() можно вызвать только у переменной, которая гарантированно имеет тип нужной ссылки. Если реквизит строго типизирован (например, только этот справочник), то по умолчанию там хранится пустая ссылка.Неопределено. Этот тип данных указывает на то, что тип значения вообще еще не задан. Такое часто происходит с реквизитами составного типа данных (когда поле может содержать ссылки на разные справочники или документы). Также значение Неопределено возвращается для реквизитов нового, еще ни разу не записанного объекта (как в примере из сообщения №9 с контрагентами). Пытаясь вызвать метод Пустая() у значения Неопределено, вы неизбежно получите ошибку объектного типа, так как у типа Неопределено нет и не может быть такого метода.NULL. Это специальный тип данных, используемый СУБД для обозначения отсутствующих данных (например, при левых соединениях в запросах). Напрямую в реквизитах объектов базы данных он практически не встречается, но часто фигурирует в результатах запросов. У NULL также нет никаких методов, и попытка обратиться к нему вызовет аналогичную ошибку.Для того чтобы обезопасить свой код от непредвиденных типов данных и возможных ошибок, в платформе 1С существует универсальная и очень быстрая функция — ЗначениеЗаполнено().
Давайте посмотрим, как она работает. Функция принимает на вход проверяемое значение и возвращает Истина, если значение заполнено, и Ложь в следующих случаях:
NULL или Неопределено.""), число 0, пустая дата ('00010101000000') или значение Ложь для булевых типов.Таким образом, проверка на незаполненный реквизит должна выглядеть следующим образом:
Если НЕ ЗначениеЗаполнено(Объект.БазоваяЕдиницаИзмерения) Тогда
// Поле пустое, здесь пишем код заполнения
КонецЕсли;
Это самый безопасный, надежный и рекомендуемый стандарт разработки на платформе «1С:Предприятие 8».
Теперь разберем вторую проблему: как присвоить ссылочному полю нужное значение. Как мы уже выяснили, просто написать число или строку нельзя. Платформе нужна ссылка на элемент справочника КлассификаторЕдиницИзмерения.
Чтобы получить эту ссылку, нам нужно найти элемент в базе данных. Чаще всего поиск осуществляют по коду или по наименованию. Обратите внимание, что коды в справочниках 1С почти всегда имеют текстовый тип (иногда с ведущими нулями). Поэтому передавать в поиск нужно именно строку:
// Находим ссылку на элемент классификатора по его коду (796 - штуки)
СсылкаНаЕдиницу = Справочники.КлассификаторЕдиницИзмерения.FindByCode("796"); // Англоязычный аналог НайтиПоКоду
// Проверяем, нашли ли мы элемент в базе
Если ЗначениеЗаполнено(СсылкаНаЕдиницу) Тогда
Объект.БазоваяЕдиницаИзмерения = СсылкаНаЕдиницу;
Иначе
Сообщить("Не удалось найти единицу измерения с кодом 796 в классификаторе!");
КонецЕсли;
Давайте проанализируем архитектурную проблему первоначального кода. Использование Выбрать() для всего справочника Номенклатура с последующим вызовом ПолучитьОбъект() — это классический антипаттерн, приводящий к колоссальным нагрузкам на сервер. Представьте, что в справочнике 100 000 позиций. Программа прочитает из базы данных все 100 000 элементов, создаст для каждого тяжелый объект в памяти сервера, и только для нескольких штук применит изменения.
Правильный подход: сначала отфильтровать данные с помощью запроса на сервере базы данных (СУБД) и выбрать только те ссылки, у которых реквизит действительно не заполнен и которые не являются группами. И только по этим отфильтрованным ссылкам получать объекты для перезаписи.
Проанализируем оптимальный код решения этой задачи:
// 1. Создаем запрос для выборки только нужных элементов
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа
| И Номенклатура.БазоваяЕдиницаИзмерения = ЗНАЧЕНИЕ(Справочник.КлассификаторЕдиницИзмерения.ПустаяСсылка)";
РезультатЗапроса = Запрос.Выполнить();
// Если запрос вернул пустой результат, то и делать ничего не нужно
Если НЕ РезультатЗапроса.Пустой() Тогда
// Находим элемент для заполнения один раз перед циклом!
ЕдиницаШтуки = Справочники.КлассификаторЕдиницИзмерения.НайтиПоКоду("796");
Если ЗначениеЗаполнено(ЕдиницаШтуки) Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Получаем объект только для тех элементов, которые точно требуют изменения
Попытка
ОбъектНоменклатуры = Выборка.Ссылка.ПолучитьОбъект();
ОбъектНоменклатуры.БазоваяЕдиницаИзмерения = ЕдиницаШтуки;
ОбъектНоменклатуры.Записать();
Исключение
// Обязательно обрабатываем возможные блокировки или ошибки прав доступа
ЗаписьЖурналаРегистрации("ОбновлениеНоменклатуры",
УровеньЖурналаРегистрации.Ошибка,
ОбъектНоменклатуры.Метаданные(),
Выборка.Ссылка,
ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
Иначе
ВызватьИсключение "В классификаторе не найдена единица измерения с кодом 796!";
КонецЕсли;
КонецЕсли;
Этот код отработает в десятки раз быстрее исходного варианта, поскольку база данных вернет только целевые записи, а серверу 1С не придется тратить ресурсы на обработку заполненных элементов и папок группы.
В ходе обсуждения в сообщении №7 возник дополнительный вопрос: как ограничить обработку и заполнять единицы измерения только для товаров, находящихся в определенной папке (и ее подпапках) справочника Номенклатура?
Для решения этой задачи в запросах 1С используется мощный оператор В ИЕРАРХИИ. Он позволяет автоматически выбрать все вложенные элементы на любом уровне вложенности относительно указанной группы-родителя.
Рассмотрим пример реализации с отбором по конкретной папке:
// 1. Находим нужную целевую папку (например, по коду или по наименованию)
ЦелеваяПапка = Справочники.Номенклатура.НайтиПоКоду("УТ-000015");
Если НЕ ЗначениеЗаполнено(ЦелеваяПапка) Тогда
ВызватьИсключение "Указанная группа номенклатуры не найдена в базе данных!";
КонецЕсли;
// 2. Формируем запрос с условием "В ИЕРАРХИИ"
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Ссылка В ИЕРАРХИИ(&ЦелеваяПапка)
| И НЕ Номенклатура.ЭтоГруппа
| И Номенклатура.БазоваяЕдиницаИзмерения = ЗНАЧЕНИЕ(Справочник.КлассификаторЕдиницИзмерения.ПустаяСсылка)";
Запрос.УстановитьПараметр("ЦелеваяПапка", ЦелеваяПапка);
РезультатЗапроса = Запрос.Выполнить();
// 3. Обходим только отобранные элементы внутри иерархии папки
Если НЕ РезультатЗапроса.Пустой() Тогда
ЕдиницаШтуки = Справочники.КлассификаторЕдиницИзмерения.НайтиПоКоду("796");
Если ЗначениеЗаполнено(ЕдиницаШтуки) Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Попытка
ОбъектНоменклатуры = Выборка.Ссылка.ПолучитьОбъект();
ОбъектНоменклатуры.БазоваяЕдиницаИзмерения = ЕдиницаШтуки;
ОбъектНоменклатуры.Записать();
Исключение
Сообщить("Не удалось записать элемент: " + Выборка.Ссылка + ". Ошибка: " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецЕсли;
КонецЕсли;
Использование этого метода гарантирует, что изменения затронут исключительно товары внутри выбранной папки (включая все подпапки любой глубины), а структура групп останется нетронутой благодаря фильтру НЕ Номенклатура.ЭтоГруппа.