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