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