В практике разработки на платформе 1С часто возникает задача: в момент проведения документа рассчитать некие показатели (например, суммы проводок, налоги или себестоимость) и сохранить результат обратно в табличную часть этого же документа. На первый взгляд задача кажется тривиальной, но она таит в себе серьезные архитектурные ловушки, связанные с транзакционной моделью 1С. Разберем подробно, почему классическая «двойная запись» — это плохо, и какие существуют правильные способы решения этой задачи.
Прежде всего, проанализируем ситуацию с точки зрения платформы. Процедура ОбработкаПроведения вызывается внутри транзакции записи объекта. Если мы попытаемся внутри этой процедуры вызвать метод ЭтотОбъект.Записать(), то столкнемся с одной из двух проблем:
Рассмотрим, как обойти эти ограничения и сделать работу системы стабильной.
Самый правильный и «чистый» с точки зрения архитектуры способ — это выполнение всех расчетов до того, как данные попадут в базу данных. Если результат расчета в табличной части зависит только от данных самого документа (строк, реквизитов шапки), то нам не нужно дожидаться проведения.
Разберем алгоритм действий:
ПередЗаписью.ЭтотОбъект.ИмяТабличнойЧасти.Посмотрим на пример кода:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Расчет выполняется только при проведении или просто записи
Для Каждого СтрокаТЧ Из Товары Цикл
СтрокаТЧ.СуммаРасчетная = СтрокаТЧ.Количество * Константы.КоэффициентСложности.Получить();
КонецЦикла;
КонецПроцедуры
При таком подходе данные в табличной части обновляются в той же самой транзакции, в которой записывается документ. Никакой второй записи не требуется, и в ОбработкаПроведения мы уже имеем актуальные данные для формирования движений.
Иногда расчет невозможен до записи, так как он зависит от итогов в регистрах, которые сформируются только в процессе проведения (например, расчет партий). В этом случае мы можем использовать механизм временного хранения данных в памяти объекта.
Проанализируем ситуацию: в ОбработкаПроведения мы делаем расчет и помещаем его в ДополнительныеСвойства. Это специальная структура, которая «живет» в объекте до конца серверного вызова.
// В модуле объекта
Процедура ОбработкаПроведения(Отказ, РежимПроведения)
// ... логика проведения ...
СуммаДляВозврата = Движения.Хозрасчетный.Итог("Сумма");
ЭтотОбъект.ДополнительныеСвойства.Вставить("РезультатРасчета", СуммаДляВозврата);
КонецПроцедуры
Затем в модуле формы или в обработчике ПослеЗаписи на сервере мы можем извлечь это значение. Однако, если нам нужно именно сохранить это в БД в табличную часть, нам все равно потребуется вторая запись. Чтобы сделать её максимально «безболезненной», используем флаг ОбменДанными.Загрузка.
Если бизнес-логика требует, чтобы данные в ТЧ появились только после расчета движений, мы можем прибегнуть к методу «второго прохода». Но делать это нужно крайне осторожно, чтобы не вызвать перепроведение документа.
Разберем по шагам, как реализовать это в модуле документа:
ОбработкаПроведения вычисляем данные.ДополнительныеСвойства.ПриЗаписи (которое срабатывает после ОбработкаПроведения, но внутри транзакции) или лучше во внешнем коде после завершения транзакции, инициируем обновление.Важный нюанс: при повторной записи нужно отключать все проверки, чтобы не тратить ресурсы системы. Выясним причину использования ОбменДанными.Загрузка = Истина: этот флаг велит платформе пропустить процедуры ПередЗаписью и ПриЗаписи, не очищать движения и не выполнять бизнес-валидацию.
// Пример записи "извне" или из фонового задания
ДокОбъект = СсылкаНаДокумент.ПолучитьОбъект();
ДокОбъект.ОбменДанными.Загрузка = Истина;
// Модифицируем ТЧ
ДокОбъект.Товары[0].Сумма = НовоеЗначение;
ДокОбъект.Записать(РежимЗаписиДокумента.Запись);
Рассмотрим наиболее зрелый подход. Если данные являются результатом проведения (движениями), то их не нужно хранить в документе. Документ — это регистратор первичной информации. Регистр — это место хранения итогов и расчетов.
Проанализируем минусы хранения результатов расчета в ТЧ:
_DocumentX, что мешает другим пользователям.Рекомендация: если вам нужно показать эти данные в форме документа, просто выведите их запросом из регистра при открытии формы или при нажатии кнопки «Обновить данные».
Выясним, что происходит с базой данных при двойной записи. Когда пользователь нажимает «Провести», система открывает транзакцию. В этот момент блокируются записи в регистрах и в таблице документа. Если мы добавляем вторую запись сразу после первой, мы удлиняем время транзакции. В высоконагруженных системах это приводит к ошибке Deadlock (взаимоблокировка).
Посмотрим на пример ситуации: Пользователь А проводит документ и пытается обновить его ТЧ. Пользователь Б в это же время пытается провести другой документ, который считывает данные из ТЧ первого документа. Оба ждут друг друга, система зависает.
Чтобы избежать этого, разберем паттерн отложенного обновления. Мы можем запустить фоновое задание, которое выполнит расчет и запишет данные в ТЧ через несколько секунд после того, как основная транзакция проведения будет успешно завершена. Это освободит интерфейс пользователя мгновенно.
Проанализировав все способы, мы можем составить итоговый чек-лист для разработчика:
ПередЗаписью. Это идеальный вариант.ОбменДанными.Загрузка = Истина и стараемся вынести эту операцию за пределы основной транзакции проведения (например, в фоновое задание).Помните, что архитектура «документ — это только исходные данные, регистр — это результат» является фундаментом производительности любой системы на базе 1С:Предприятие 8.3.