Как организовать каскадное перепроведение связанных документов в 1С и избежать ошибки «Версия объекта изменилась»?

Программист 1С v8.3 (Управляемые формы) Управленческий учет IT и автоматизация бизнеса
← На главную

Переход с платформы 1С:Предприятие 7.7 на 1С:Предприятие 8 часто сопровождается трудностями, связанными с изменением базовой архитектуры работы с базой данных. Типичный пример — задача каскадного перепроведения цепочки документов (например, основного договора и подчиненных ему дополнительных соглашений) при изменении любого из них. Разработчики, привыкшие к механизмам «семерки», часто пытаются решить эту задачу «в лоб» через транзакции записи, сталкиваясь с неприятной ошибкой: «Редактируемый объект изменен или удален другим пользователем (версия объекта изменилась)».

В этой статье мы подробно разберем, почему возникает эта ошибка оптимистической блокировки, как исправить ее в текущем коде с помощью механизма дополнительных свойств и какие современные архитектурные паттерны (Best Practices) следует использовать для решения подобных задач в платформе 1С 8.3.

Почему возникает ошибка оптимистической блокировки?

Для начала выясним первопричину проблемы. В платформе 1С 8.х реализован механизм оптимистической блокировки объектов. Его суть заключается в следующем:

  1. Когда пользователь открывает форму документа или когда код считывает объект из базы данных, платформа запоминает текущую версию этого объекта (уникальный внутренний идентификатор версии).
  2. При попытке записать изменения платформа сравнивает версию записываемого объекта в памяти с версией этого же объекта, которая хранится непосредственно в таблице базы данных.
  3. Если версии совпадают, запись проходит успешно, и версия объекта инкрементируется (обновляется).
  4. Если же в процессе выполнения транзакции записи (например, в обработчиках событий ПриЗаписи, ОбработкаПроведения или ПослеЗаписи) сторонняя процедура считывает этот же документ из базы через метод ПолучитьОбъект(), как-то модифицирует его и записывает, то версия объекта в базе данных меняется раньше, чем завершится основная транзакция исходного объекта.
  5. При попытке завершить основную транзакцию исходного объекта платформа видит несоответствие версий и откатывает всю транзакцию с ошибкой «Версия объекта изменилась».

Когда мы пытаемся при проведении подчиненного документа запустить процедуру полного перепроведения всей цепочки (включая этот самый подчиненный документ), мы гарантированно попадаем в эту ловушку. Система пытается повторно записать объект, который прямо сейчас находится в процессе записи.

Рассмотрим детально три основных способа решения этой проблемы, начиная от локального исправления кода и заканчивая правильной архитектурной перестройкой логики приложения.

Решение 1. Использование «ДополнительныхСвойств» для обхода рекурсии

Если вам необходимо решить задачу быстро, без глобального изменения логики конфигурации, мы можем использовать специальное свойство любого объекта 1С — коллекцию ДополнительныеСвойства. Это структура, которая существует только во время жизненного цикла объекта в оперативной памяти и не записывается в базу данных. С ее помощью мы можем передавать «флаги» между различными обработчиками событий и избегать бесконечной рекурсии при каскадной записи связанных документов.

Разберем этот алгоритм по шагам:

  1. В модуле подчиненных и основных документов (или в подписке на события) перед записью мы проверяем, установлен ли наш специализированный флаг в коллекции ДополнительныеСвойства.
  2. Если флаг отсутствует, это означает, что запись инициирована пользователем вручную. Мы должны программно получить всю цепочку документов, установить в их дополнительные свойства наш флаг блокировки повторного вызова и провести их.
  3. Если флаг уже установлен, мы просто пропускаем каскадный вызов, чтобы избежать зацикливания.

Посмотрим на пример программной реализации этого подхода в обработчике события ПриЗаписи:


Процедура ПриЗаписи(Отказ)
    
    // Проверяем, не запущено ли уже каскадное перепроведение этой группы
    Если Проведен И Не ДополнительныеСвойства.Свойство("ПерезаписьГруппыДокументов") Тогда
        
        // Создаем структуру параметров для передачи в подчиненные объекты
        ПараметрыПерезаписи = Новый Структура("Источник", Ссылка);
        
        // Получаем список всех связанных документов (основного и подчиненных)
        СписокДокументов = ПолучитьСписокСвязанныхДокументов(Ссылка);
        
        Для Каждого СтрокаДокумента Из СписокДокументов Цикл
            
            // Если документ в списке — это текущий документ, пропускаем его,
            // так как платформа сама запишет его в конце текущей транзакции
            Если СтрокаДокумента.Ссылка = Ссылка Тогда
                Продолжить;
            КонецЕсли;
            
            // Получаем объект связанного документа для записи
            ДокументОбъект = СтрокаДокумента.Ссылка.ПолучитьОбъект();
            
            // Внедряем флаг в дополнительные свойства связанного объекта
            ДокументОбъект.ДополнительныеСвойства.Вставить("ПерезаписьГруппыДокументов", ПараметрыПерезаписи);
            
            Попытка
                // Проводим связанный документ программно
                ДокументОбъект.Записать(РежимЗаписиДокумента.Проведение);
            Исключение
                // Обязательно обрабатываем исключения, чтобы не заблокировать работу пользователя
                ЗаписьЖурналаРегистрации("КаскадноеПроведение",
                    УровеньЖурналаРегистрации.Ошибка,,,
                    ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
                Отказ = Истина;
                Прервать;
            КонецПопытки;
            
        КонецЦикла; 
        
    КонецЕсли; 
    
КонецПроцедуры

Важное примечание: Обратите внимание на условие исключения текущего документа из цикла перепроведения (Если СтрокаДокумента.Ссылка = Ссылка Тогда Продолжить;). Это ключевой момент, который убережет вас от ошибки изменения версии текущего объекта.

Решение 2. Прямая запись движений в регистры (Архитектурно правильный быстрый путь)

Рассмотрим более глубокий анализ бизнес-логики, представленной в обсуждении. Зачастую разработчики используют перепроведение документов только ради одной цели — перестроить записи в периодическом регистре сведений (например, графики аренды или действующие соглашения). При этом сами реквизиты документов никак не меняются.

В платформе 1С 8.3 проведение документов «в цепочке» ради актуализации регистра — это явный антипаттерн, приводящий к избыточной нагрузке на СУБД и конфликтам блокировок. Вместо этого мы можем использовать прямую запись движений по регистратору.

Выясним, как это работает:

  1. Документы остаются в своем текущем состоянии проведения, их физически никто не трогает и не перезаписывает.
  2. В общем модуле пишется процедура, которая рассчитывает актуальные периоды действия аренды для всей цепочки документов сразу на основании данных из базы данных.
  3. Для каждого документа-регистратора программно создается объект РегистрСведенийНаборЗаписей с установленным отбором по конкретному документу.
  4. Набор записей заполняется новыми, актуальными и скорректированными строками и записывается методом Записать(Истина).

Проанализируем пример кода, реализующий этот подход без перепроведения самих документов:


Процедура АктуализироватьРегистрАрендыПоДоговору(СсылкаНаДоговор) Экспорт
    
    // Шаг 1. Получаем таблицу актуальных периодов по всей цепочке договора
    ТаблицаПериодов = РассчитатьПериодыАрендыПоДоговору(СсылкаНаДоговор);
    
    // Шаг 2. Группируем данные по регистраторам, чтобы обновить движения точечно
    Для Каждого СтрокаДоговора Из ТаблицаПериодов.СкопироватьКолонку("Регистратор") Цикл
        
        // Создаем набор записей для конкретного регистратора (документа)
        НаборЗаписей = РегистрыСведений.ТекущийДокументАренды.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Регистратор.Установить(СтрокаДоговора.Регистратор);
        
        // Находим строки, относящиеся к этому регистратору
        СтрокиДляЗаписи = ТаблицаПериодов.НайтиСтроки(Новый Структура("Регистратор", СтрокаДоговора.Регистратор));
        
        НаборЗаписей.Очистить();
        Для Каждого СтрокаДанных Из СтрокиДляЗаписи Цикл
            НоваяЗапись = НаборЗаписей.Добавить();
            ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаДанных);
        КонецЦикла;
        
        // Записываем движения напрямую, не трогая сам документ
        НаборЗаписей.Записать(Истина);
        
    КонецЦикла;

КонецПроцедуры

При таком подходе пользователи вообще не заметят задержек, транзакция записи документа пройдет мгновенно, а данные в регистре сведений всегда будут в актуальном и непротиворечивом состоянии.

Решение 3. Использование очереди проведения и фоновых заданий

В высоконагруженных многопользовательских системах проведение связанных документов непосредственно в момент записи пользователем может полностью заблокировать работу всей базы данных. Если один документ блокирует таблицы при проведении, другие пользователи не смогут провести свои документы по этой же организации или договору.

Наиболее современным стандартом разработки в среде 1С (принятым в типовых решениях уровня ERP и УТ) является отложенное проведение через очередь. Схема выглядит следующим образом:

  1. Создается простой и легкий независимый регистр сведений, например, ОчередьПересчетаДоговоров с единственным измерением Договор типа ДокументСсылка (или СправочникСсылка).
  2. При проведении или изменении любого документа (основного или подчиненного) в этот регистр в рамках транзакции проведения пишется всего одна строка: ссылка на головной договор, который требует актуализации данных. Это быстрая операция, занимающая миллисекунды.
  3. В системе настраивается регулярное регламентное (фоновое) задание, которое запускается по расписанию (например, каждые 10 секунд или раз в минуту в зависимости от бизнес-требований).
  4. Фоновое задание считывает накопленные в очереди договоры, блокирует их на время обработки, поочередно выполняет полное каскадное перепроведение всей цепочки документов (или обновление движений регистров по алгоритму из Варианта 2) в изолированной фоновой транзакции и удаляет обработанные записи из регистра очереди.

Этот подход гарантирует высокую скорость отклика системы для конечных пользователей и полностью исключает возникновение взаимных блокировок СУБД.

Резюме и рекомендации

При проектировании механизмов каскадного проведения документов в 1С:Предприятии 8 всегда соотносите техническое решение с реальными потребностями бизнеса и архитектурными ограничениями платформы:

← На главную