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