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