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

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

В процессе многопользовательской работы в среде 1С:Предприятие 8 разработчики часто сталкиваются с неприятной ситуацией. Пользователь открывает форму документа и начинает его редактировать. В этот же самый момент другой пользователь (или фоновое задание, или алгоритм проведения связанного документа) программно изменяет и записывает этот же самый документ в базе данных. Когда первый пользователь пытается нажать кнопку «Записать» или «Провести», система возвращает критическую ошибку: «Данные изменены или удалены другим пользователем». Это классическое проявление механизма оптимистической блокировки.

В этой статье мы подробно разберем, почему возникает данное поведение, как устроен этот механизм на уровне СУБД, а также рассмотрим практические способы решения этой проблемы — от быстрых программных обходов до правильного архитектурного рефакторинга.

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

Как устроена оптимистическая блокировка «под капотом»

Прежде чем переходить к написанию кода, давайте разберем физический смысл оптимистической блокировки. В отличие от пессимистической блокировки, которая «захватывает» объект в базе данных и не позволяет другим процессам изменять его до окончания транзакции, оптимистическая блокировка работает иначе. Она ничего заранее не блокирует, предполагая, что конфликты совместного доступа происходят относительно редко.

Контроль версий в 1С базируется на системном реквизите ВерсияДанных. На уровне СУБД (например, в MS SQL Server) это поле представляет собой автоматически увеличивающийся счетчик изменений типа rowversion или timestamp. Схема работы выглядит следующим образом:

  1. При открытии формы документа платформа считывает данные из таблицы СУБД в оперативную память сервера приложений и передает их на клиент. Вместе с реквизитами передается текущее значение ВерсияДанных (например, "AAAAAAADp8=").
  2. Пользователь начинает интерактивно вносить правки в форму. Значение в базе данных пока остается прежним.
  3. В этот момент сторонний процесс (например, проведение другого документа) программно считывает этот же документ, меняет в нем один реквизит (например, статус) и записывает. СУБД при записи автоматически обновляет версию данных этого документа (теперь она становится, например, "AAAAAAADqA=").
  4. Первый пользователь заканчивает редактирование и нажимает кнопку «Записать». Платформа 1С пытается записать измененные данные формы обратно в СУБД. Перед записью выполняется сравнение: версия данных, сохраненная в форме ("AAAAAAADp8="), сопоставляется с текущей версией в таблице базы данных ("AAAAAAADqA=").
  5. Поскольку версии различаются, система понимает, что пользователь редактировал устаревшие данные и может случайно затереть изменения, сделанные параллельным процессом. Транзакция откатывается, и генерируется ошибка оптимистической блокировки.

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

Вариант 1. Паттерн «Копирование — Перечитывание — Восстановление»

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

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

Проанализируем пример кода, который реализует эту логику на стороне управляемой формы документа:


&НаКлиенте
Процедура ОбновитьФормуССохранениемДанных()
    
    // Шаг 1: Проверяем, модифицирована ли форма. 
    // Если пользователь ничего не менял, мы можем просто перечитать объект без лишних ухищрений.
    Если Не ЭтаФорма.Модифицированность Тогда
        ЭтаФорма.Прочитать();
        Возврат;
    КонецЕсли;
    
    // Шаг 2: Если изменения есть, сохраняем пользовательские данные во временную структуру.
    // Нам нужно сохранить только те реквизиты, которые пользователь мог изменить вручную.
    СтруктураПользовательскихПравок = Новый Структура;
    СтруктураПользовательскихПравок.Вставить("Комментарий", Объект.Комментарий);
    СтруктураПользовательскихПравок.Вставить("СуммаДокумента", Объект.СуммаДокумента);
    
    // Для табличных частей можно выгрузить их во временные таблицы значений
    ТаблицаТовары = Объект.Товары.Выгрузить();
    
    // Шаг 3: Перечитываем объект из базы данных. 
    // Метод Прочитать() обновит данные на форме и запишет актуальную ВерсиюДанных из СУБД.
    // При этом пользовательские изменения в реквизите "Объект" затрутся.
    Попытка
        ЭтаФорма.Прочитать();
    Исключение
        ПоказатьПредупреждение(, "Не удалось обновить документ для записи. Возможно, он был удален.");
        Возврат;
    КонецПопытки;
    
    // Шаг 4: Восстанавливаем сохраненные изменения пользователя поверх актуальных данных.
    // Заполняем реквизиты шапки
    ЗаполнитьЗначенияСвойств(Объект, СтруктураПользовательскихПравок);
    
    // Восстанавливаем табличную часть
    Объект.Товары.Загрузить(ТаблицаТовары);
    
    // Шаг 5: Устанавливаем признак модифицированности, так как мы вернули изменения на форму
    ЭтаФорма.Модифицированность = Истина;
    
    // Теперь форму можно безопасно записывать — версия данных актуальна!
КонецПроцедуры

Этот метод хорошо работает, если количество изменяемых пользователем реквизитов невелико. Если же форма сложная и содержит множество реквизитов, ручной перенос может стать трудоемким. В таком случае можно использовать универсальное копирование данных формы через создание структуры-копии на сервере с помощью метода КопироватьДанныеФормы, однако при обратном копировании необходимо строго следить за тем, чтобы не скопировать обратно старую ВерсияДанных.

Вариант 2. Использование пессимистической блокировки для предотвращения конфликта

Зачастую оптимистическая блокировка срабатывает из-за того, что разработчики забывают синхронизировать интерактивные и программные действия над объектами. Если у одного пользователя открыта форма документа, платформа по умолчанию накладывает на этот документ интерактивную пессимистическую блокировку (при условии, что у реквизитов формы взведен флаг «Сохраняемые данные»).

Однако эта блокировка защищает только от параллельного редактирования документа *другими пользователями через интерфейс*. Программный код, запущенный в другом сеансе (например, при проведении другого документа), по умолчанию игнорирует интерактивную блокировку формы и без проблем перезаписывает документ в базе данных.

Чтобы избежать этой коллизии, при программном изменении связанных документов всегда используйте явное наложение блокировки. Рассмотрим, как правильно изменять связанный документ (например, "Документ-Приемник") из модуля проведения "Документа-Источника":


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

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

Вариант 3. Вынос изменяемых реквизитов за рамки объекта (Регистр сведений)

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

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

Преимущества архитектуры с регистром сведений:

Посмотрим на пример реализации отображения и изменения статуса на управляемой форме документа без модификации самого документа:


&НаСервере
Процедура ПриЧтенииНаСервере(ТекущийОбъект)
    // Читаем актуальный статус из регистра сведений при открытии формы
    // СтатусДокумента - это обычный реквизит формы (не реквизит объекта!)
    СтатусДокумента = РегистрыСведений.СтатусыДокументов.ПолучитьПоследнее(
        ТекущийОбъект.Ссылка
    ).Статус;
КонецПроцедуры

&НаКлиенте
Процедура ИзменитьСтатусДокумента(НовыйСтатус)
    // Записываем новый статус напрямую в базу данных, минуя запись самого документа
    ЗаписатьСтатусВРегистрНаСервере(Объект.Ссылка, НовыйСтатус);
    СтатусДокумента = НовыйСтатус;
КонецПроцедуры

&НаСервереБезКонтекста
Процедура ЗаписатьСтатусВРегистрНаСервере(СсылкаНаДокумент, НовыйСтатус)
    МенеджерЗаписи = РегистрыСведений.СтатусыДокументов.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.Документ = СсылкаНаДокумент;
    МенеджерЗаписи.Статус = НовыйСтатус;
    МенеджерЗаписи.Период = ТекущаяДата();
    МенеджерЗаписи.Записать(Истина);
КонецПроцедуры

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

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

Мы проанализировали причины возникновения конфликтов блокировок и рассмотрели три кардинально разных метода их решения. Подведем итоги и определим, какой метод использовать в вашей повседневной практике:

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