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

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

В процессе разработки интеграций через HTTP-сервисы или Web-сервисы программисты часто сталкиваются с ситуацией, когда метод Записать() выбрасывает исключение: «Ошибка при вызове метода контекста (Записать): Операция не может быть выполнена из-за несоответствия версии или отсутствия записи базы данных (возможно, запись была изменена или удалена)!». Особенно часто это проявляется при попытке обновить статус обмена непосредственно в узле плана обмена или в реквизите справочника.

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

Разбор причины: механизм «Версия данных» (DataVersion)

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

Рассмотрим стандартный цикл изменения объекта в коде:

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

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

Почему нельзя использовать реквизиты объекта для хранения статусов обмена

Давайте выясним, почему запись в ПланыОбмена или Справочники для фиксации технических статусов — это плохая практика:

  1. Инвалидация кэша: Запись любого объекта метаданных сбрасывает кэш объектов для всех сеансов пользователей. Если ваш обмен работает интенсивно (например, раз в секунду), постоянная перезапись узла плана обмена приведет к существенному замедлению всей системы, так как 1С будет вынуждена постоянно перечитывать данные из базы.
  2. Высокая вероятность блокировок: Объекты метаданных — это тяжелые структуры. Конкуренция за их изменение создает очереди в базе данных.
  3. Отсутствие истории: Записывая статус в реквизит, вы видите только последнее состояние. Если обмен завершился ошибкой, а затем успешно повторился, вы никогда не узнаете о промежуточном сбое без анализа Журнала регистрации.

Решение №1: Использование Регистров сведений (Рекомендуемое)

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

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


МенеджерЗаписи = РегистрыСведений.СтатусыОбмена.СоздатьМенеджерЗаписи();
МенеджерЗаписи.УзелИнтеграции = СсылкаНаУзел;
МенеджерЗаписи.ДатаСобытия = ТекущаяДата();
МенеджерЗаписи.Статус = Перечисления.СтатусыОбмена.Успешно;
МенеджерЗаписи.Сообщение = "Обмен завершен без ошибок";
МенеджерЗаписи.Записать(Истина); // Параметр Истина означает замещение существующей записи

Если вам нужно хранить историю, сделайте регистр периодическим или добавьте измерение ИдентификаторЗапроса. Это позволит избежать любых конфликтов при записи данных из параллельных потоков HTTP-сервиса.

Решение №2: Использование управляемых блокировок (DataLock)

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

Разберем, как правильно установить блокировку перед изменением узла:


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("ПланОбмена.СинхронизацияДанныхЧерезУниверсальныйФормат");
ЭлементБлокировки.УстановитьЗначение("Ссылка", УзелСсылка);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;

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

Важно помнить, что метод Блокировка.Заблокировать() должен вызываться внутри транзакции. Только так вы обеспечите целостность данных при высокой нагрузке.

Решение №3: Паттерн «Повторное чтение» в блоке Попытка

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

  1. Пытаемся записать объект.
  2. Если получаем ошибку «Несоответствие версии», заново вызываем ПолучитьОбъект().
  3. Снова устанавливаем нужные реквизиты (так как в новом объекте они будут старыми).
  4. Повторяем попытку записи.

Пример реализации:


УзелОбъект = УзелСсылка.ПолучитьОбъект();
УзелОбъект.Статус = "Выполняется";

Попытка
    УзелОбъект.Записать();
Исключение
    // Возможно, версия изменилась. Попробуем еще раз.
    УзелОбъект = УзелСсылка.ПолучитьОбъект(); 
    УзелОбъект.Статус = "Выполняется";
    УзелОбъект.Записать();
КонецПопытки;

Внимание: Этот метод считается «костылем» и не рекомендуется для постоянного использования, так как он не решает проблему конкуренции, а лишь маскирует её, увеличивая нагрузку на сервер.

Подведем итоги

Для стабильной работы интеграций в 1С придерживайтесь следующих правил:

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

← На главную