Когда мы работаем с документами в 1С:Предприятии, особенно в ситуациях, когда их формы открыты, возникает вероятность одновременного редактирования несколькими пользователями или фоновыми процессами. Это может привести к конфликтам данных и ошибкам при записи. Давайте вместе разберемся, как обойти типичные проблемы и обеспечить стабильную работу системы.
Прежде чем перейти к решениям, давайте рассмотрим, как 1С:Предприятие обеспечивает целостность данных с помощью механизмов блокировок. Это поможет нам лучше понять причины возникающих ошибок и выбрать наиболее подходящую стратегию их обхода. Система использует два основных вида объектных блокировок: оптимистическую и пессимистическую.
Оптимистическая блокировка — это проверка, которая выполняется непосредственно перед записью объекта в базу данных. Когда мы считываем объект из базы, вместе с данными считывается и его версия, которая хранится в базе данных в свойстве ВерсияДанных. Если к моменту записи объекта в базу данных его ВерсияДанных изменилась (например, другой пользователь уже сохранил свои изменения), система выдаст сообщение об ошибке, так как версии не совпадают. Это гарантирует, что изменения одного пользователя не "затрут" изменения, сделанные другими сессиями. Свойство ВерсияДанных автоматически обновляется при каждой успешной записи объекта.
Пессимистическая блокировка предназначена для того, чтобы запретить изменение данных объекта другими сеансами или текущим сеансом до тех пор, пока блокировка не будет снята. В основном, этот механизм используется системой 1С:Предприятие для блокировки объектов, редактируемых в формах. Когда пользователь начинает модификацию объекта в форме, расширение формы автоматически устанавливает пессимистическую блокировку. Если другой пользователь попытается редактировать тот же объект, ему будет выдано сообщение о невозможности блокировки. Блокировка снимается, когда пользователь закрывает форму объекта.
Мы также можем использовать пессимистическую блокировку программно, вызывая методы Заблокировать() объекта или глобального контекста ЗаблокироватьДанныеДляРедактирования(). Метод ЗаблокироватьДанныеДляРедактирования() позволяет попытаться заблокировать данные для редактирования и, в случае неудачи (если данные уже заблокированы), вызывает исключение, информируя пользователя, кто и откуда заблокировал объект. Важно помнить, что если блокировка установлена с параметром ИдентификаторФормы, платформа не гарантирует автоматического снятия блокировки после закрытия формы. Поэтому рекомендуется явно снимать блокировку методом РазблокироватьДанныеДляРедактирования() после записи объекта или завершения алгоритма. Давайте посмотрим на пример:
Попытка
// Попытаемся заблокировать данные для редактирования
ЗаблокироватьДанныеДляРедактирования(Объект.Метаданные(), Объект.Ссылка);
// Если блокировка успешна, выполняем изменения и записываем объект
Объект.Реквизит1 = "Новое значение";
Объект.Записать();
Исключение
// Если не удалось заблокировать (объект уже заблокирован),
// информируем пользователя
Сообщить("Не удалось заблокировать документ: " + ОписаниеОшибки());
КонецПопытки;
// Важно: всегда снимать блокировку, если она была установлена программно
// В данном примере, если блокировка была успешна, ее нужно снять.
// Для упрощения примера, это не показано в блоке Исключение.
// В реальном коде используйте блок Наконец для гарантированного снятия.
Итак, мы выяснили причины возникновения ошибок. Теперь давайте разберем по шагам различные стратегии, которые помогут нам корректно обрабатывать запись документов, когда их формы открыты. Мы рассмотрим несколько подходов, каждый из которых имеет свои преимущества и области применения.
Этот подход основан на использовании оптимистической блокировки и предполагает, что мы должны проверять актуальность данных перед записью. Если данные изменились, мы уведомляем пользователя и предлагаем обновить форму. Давайте посмотрим подробнее:
Проверка ВерсияДанных: При открытии формы сохраняем текущее значение свойства ВерсияДанных объекта. Перед попыткой записи сравниваем сохраненное значение с текущим значением ВерсияДанных в базе данных. Если они не совпадают, это означает, что объект был изменен другим пользователем или фоновым процессом.
// В модуле формы при открытии (например, в обработчике "ПриСозданииНаСервере")
// Запомним текущую версию данных объекта
Если Объект.Ссылка.Пустая() Тогда
// Новый объект, версия данных еще не присвоена
ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии = Неопределено;
Иначе
// Существующий объект, получаем его версию данных
ЗначениеВерсияДанных = Объект.ПолучитьВерсиюДанных();
Если ЗначениеВерсияДанных = Неопределено Тогда
// Если версия данных не получена (например, объект удален)
ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии = Неопределено;
Иначе
ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии = ЗначениеВерсияДанных;
КонецЕсли;
КонецЕсли;
// Перед записью (например, в обработчике "ПередЗаписьюНаСервере" или в кнопке "Записать")
Если ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии <> Неопределено Тогда
// Получаем текущую версию данных из базы
ТекущаяВерсияДанныхВБазе = Объект.ПолучитьВерсиюДанных();
Если ТекущаяВерсияДанныхВБазе <> ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии Тогда
// Версии не совпадают, объект был изменен
Отказ = Истина; // Отменяем стандартную запись
Сообщить("Документ был изменен другим пользователем. Пожалуйста, перечитайте данные и повторите попытку.", СтатусСообщения.Важное);
// Можно предложить пользователю перечитать данные
// ЭтаФорма.Перечитать();
КонецЕсли;
КонецЕсли;
Использование ОбработчикОжидания: Мы можем настроить периодическую проверку актуальности данных прямо в открытой форме. Для этого используем ОбработчикОжидания, который с заданной периодичностью (например, раз в минуту) будет проверять ВерсияДанных объекта или другие ключевые реквизиты. Если изменения обнаружены, мы можем предложить пользователю обновить форму, вызвав метод Перечитать().
// В модуле формы при открытии
Процедура ПриОткрытии()
// ... ваш код инициализации ...
ПодключитьОбработчикОжидания("ПроверитьИзмененияДокумента", 60, Истина); // Проверять каждые 60 секунд
КонецПроцедуры;
// В модуле формы при закрытии (для отключения обработчика)
Процедура ПриЗакрытии()
ОтключитьОбработчикОжидания("ПроверитьИзмененияДокумента");
КонецПроцедуры;
// Обработчик ожидания
Процедура ПроверитьИзмененияДокумента()
// Получаем текущую версию данных из базы (для текущего объекта формы)
ТекущийОбъектВБазе = Объект.ПолучитьОбъект();
Если ТекущийОбъектВБазе = Неопределено Тогда
// Объект удален, информируем пользователя
Сообщить("Документ был удален.");
Закрыть();
Возврат;
КонецЕсли;
Если ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии <> Неопределено И
ТекущийОбъектВБазе.ПолучитьВерсиюДанных() <> ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии Тогда
Сообщить("Документ был изменен другим пользователем. Рекомендуется обновить форму.", СтатусСообщения.Важное);
// Здесь можно предложить пользователю диалог: "Обновить данные?"
// Если пользователь согласен, вызываем:
// ЭтаФорма.Перечитать();
// ОбновитьДополнительныеСвойстваВерсияДанных(); // Обновить сохраненную версию данных
КонецЕсли;
ТекущийОбъектВБазе.УстановитьОбъект(Неопределено); // Освобождаем объект
КонецПроцедуры;
// Вспомогательная функция для обновления сохраненной версии данных
Процедура ОбновитьДополнительныеСвойстваВерсияДанных()
Если Объект.Ссылка.Пустая() Тогда
ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии = Неопределено;
Иначе
ЭтаФорма.ДополнительныеСвойства.ВерсияДанныхПриОткрытии = Объект.ПолучитьОбъект().ПолучитьВерсиюДанных();
КонецЕсли;
КонецПроцедуры;
Для избежания блокировок пользовательского интерфейса и выполнения длительных или ресурсоемких операций, а также для обхода ситуации, когда документ заблокирован другим пользователем, мы можем использовать механизм фоновых заданий. Это особенно полезно, если операция не должна быть частью транзакции записи документа или может быть отложена. Проанализируем ситуацию:
Суть подхода: Вместо непосредственной записи в обработчике ПриЗаписи, мы записываем данные в специальный служебный РегистрСведений (своего рода очередь обработки). Затем РегламентноеЗадание будет периодически обрабатывать этот регистр и выполнять изменения документов в фоне.
Преимущества:
Реализация: Создаем РегистрСведений "ОчередьИзмененийДокументов" с измерениями, такими как ДокументСсылка, ИмяРеквизита, и ресурсами НовоеЗначение. При необходимости изменить документ, вместо прямой записи, добавляем записи в этот регистр. Затем РегламентноеЗадание периодически считывает данные из регистра, пытается получить блокировку на документ и, если удачно, вносит изменения.
// Пример добавления задачи в регистр сведений (может быть в модуле объекта или формы)
Процедура ОтправитьИзменениеВФон()
НоваяЗапись = РегистрыСведений.ОчередьИзмененийДокументов.СоздатьМенеджерЗаписи();
НоваяЗапись.ДокументСсылка = ЭтотОбъект.Ссылка;
НоваяЗапись.ИмяРеквизита = "Комментарий";
НоваяЗапись.НовоеЗначение = "Изменено фоновым заданием: " + Формат(ТекущаяДата(), "ДФ=дд.ММ.гггг ЧЧ:мм:сс");
НоваяЗапись.Записать();
КонецПроцедуры;
// Пример кода для фонового задания (в общем модуле с флагом "Сервер")
Процедура ОбработатьОчередьИзмененийДокументов() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОчередьИзмененийДокументов.ДокументСсылка,
| ОчередьИзмененийДокументов.ИмяРеквизита,
| ОчередьИзмененийДокументов.НовоеЗначение
|ИЗ
| РегистрСведений.ОчередьИзмененийДокументов КАК ОчередьИзмененийДокументов";
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Попытка
// Пытаемся получить объект для изменения
ДокументОбъект = Выборка.ДокументСсылка.ПолучитьОбъект();
Если ДокументОбъект <> Неопределено Тогда
// Устанавливаем пессимистическую блокировку, если нужно
ЗаблокироватьДанныеДляРедактирования(ДокументОбъект.Метаданные(), ДокументОбъект.Ссылка);
// Изменяем реквизит
ДокументОбъект[Выборка.ИмяРеквизита] = Выборка.НовоеЗначение;
ДокументОбъект.Записать();
// Удаляем запись из очереди после успешной обработки
МенеджерЗаписи = РегистрыСведений.ОчередьИзмененийДокументов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ДокументСсылка = Выборка.ДокументСсылка;
МенеджерЗаписи.ИмяРеквизита = Выборка.ИмяРеквизита;
МенеджерЗаписи.Удалить();
РазблокироватьДанныеДляРедактирования(ДокументОбъект.Метаданные(), ДокументОбъект.Ссылка); // Снимаем блокировку
КонецЕсли;
Исключение
// Если документ заблокирован или другая ошибка, оставляем запись в очереди
// для повторной попытки при следующем запуске регламентного задания.
// Можно добавить логирование ошибок или счетчик попыток.
ЗаписьЖурналаРегистрации(
"ОшибкаФоновойОбработки",
УровеньЖурналаРегистрации.Ошибка,
Метаданные.Документы.ДокументКонтрагента,
Выборка.ДокументСсылка,
"Не удалось обработать документ: " + ОписаниеОшибки()
);
КонецПопытки;
КонецЦикла;
КонецПроцедуры;
Если возникает ошибка "Операция не может быть выполнена из-за несоответствия версии или отсутствия записи базы данных", это означает, что объект был изменен или удален другим пользователем. В таких случаях необходимо уведомить пользователя об изменениях и предложить перечитать данные. Рассмотрим, как это сделать:
Перехват исключений: Оборачивайте вызов метода Записать() формы документа или объекта в блок Попытка...Исключение для корректной обработки таких ситуаций.
Информирование пользователя: В блоке Исключение, если ошибка связана с несоответствием версии данных, выводите пользователю сообщение. Например, "Документ был изменен другим пользователем. Хотите обновить данные и повторить запись?"
Предложение действий: В зависимости от ответа пользователя, вы можете либо вызвать Перечитать() для обновления данных в форме, либо отменить запись. Если документ был удален, предложите закрыть форму.
// Пример в обработчике команды кнопки "Записать" на форме
Процедура КомандаЗаписать(Команда)
Попытка
ЭтотОбъект.Записать(РежимЗаписиДокумента.Проведение); // Или РежимЗаписиДокумента.Запись
Сообщить("Документ успешно записан.");
Исключение
// Анализируем описание ошибки
Если Найти(ОписаниеОшибки(), "несоответствия версии") > 0 Тогда
// Ошибка из-за оптимистической блокировки
Ответ = Вопрос("Документ был изменен другим пользователем. Обновить данные и повторить запись?", РежимДиалогаВопрос.ДаНет);
Если Ответ = КодВозвратаДиалога.Да Тогда
ЭтаФорма.Перечитать(); // Обновляем данные в форме
// Можно повторить попытку записи, но лучше дать пользователю проверить изменения
// ЭтотОбъект.Записать(РежимЗаписиДокумента.Проведение);
Иначе
Сообщить("Запись отменена пользователем.");
КонецЕсли;
ИначеЕсли Найти(ОписаниеОшибки(), "отсутствия записи базы данных") > 0 Тогда
// Документ, возможно, был удален
Сообщить("Документ, возможно, был удален из базы данных. Форма будет закрыта.", СтатусСообщения.Важное);
Закрыть();
Иначе
// Другие ошибки записи
Сообщить("Произошла ошибка при записи документа: " + ОписаниеОшибки(), СтатусСообщения.Важное);
КонецЕсли;
КонецПопытки;
КонецПроцедуры;
Это решение можно назвать "извратным", но оно может быть крайне полезным для больших и сложных документов, где требуется более гранулярный контроль над изменениями и возможность "слияния" данных. Например, если в документе сканируют марки или вводят много информации, и повторное полное перечитывание документа после чьих-то изменений нежелательно.
Суть подхода: Вместо того чтобы целиком блокировать документ или заставлять пользователя перечитывать все, мы записываем изменения отдельных реквизитов в служебный РегистрСведений.
Структура регистра: Создаем РегистрСведений с измерениями: СсылкаДокумента, ИмяРеквизита. И ресурсами: ЗначениеДо, ЗначениеПосле, Пользователь, ДатаИзменения.
Механизм работы:
РегистреСведений.ОбработчикОжидания в открытой форме документа (или отдельное РегламентноеЗадание) периодически проверяет этот регистр на наличие изменений, относящихся к текущему документу.ПриИзменении программно.Этот подход позволяет избежать полного переоткрытия документа и значительно улучшает пользовательский опыт в сложных сценариях, но требует более тщательной реализации, особенно для обработки зависимостей между реквизитами и вызова соответствующих обработчиков.
Мы рассмотрели различные стратегии для решения проблемы записи документа с открытой формой. Выбор конкретного подхода зависит от сложности документа, требований к скорости реакции системы, а также от ожиданий пользователей. В большинстве случаев комбинация проверки ВерсияДанных с уведомлением пользователя и использованием фоновых заданий для отложенных операций является наиболее гибким и надежным решением.