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