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