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