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