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