В процессе разработки прикладных решений на платформе 1С:Предприятие часто возникает задача обеспечения уникальности тех или иных данных. Это может быть уникальный штрихкод номенклатуры, ИНН контрагента, серийный номер оборудования или любой другой пользовательский реквизит справочника или документа. Если стандартных механизмов платформы (таких как уникальность кодов справочников или номеров документов) недостаточно, разработчику приходится реализовывать эту проверку программно.
В этой статье мы подробно рассмотрим несколько способов контроля уникальности реквизитов, проанализируем потенциальные проблемы параллельной работы пользователей (гонки данных) и разберем, как правильно оптимизировать этот процесс для высоконагруженных систем.
Разберем классический способ, который чаще всего применяется в типовых конфигурациях. Его суть заключается в том, чтобы перед непосредственной записью объекта в базу данных выполнить запрос к таблице этого же объекта и проверить, нет ли в базе другого элемента с таким же значением проверяемого реквизита.
Выясним причину, почему эту проверку нужно делать именно в модуле объекта. Если выполнять проверку только в форме, то при программном создании объектов (например, при загрузке данных из внешних источников, интеграции через API или обработках) проверка будет просто проигнорирована. Размещение кода в модуле объекта гарантирует, что контроль сработает при любом способе записи. Например, при миграции объектов сопоставить элементы по уникальным реквизитам и избежать дублирования можно через перенос данных между похожими конфигурациями 1С.
Рассмотрим пример реализации в модуле справочника. Нам необходимо проверить уникальность реквизита УникальныйКод перед записью элемента:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Если объект записывается в режиме загрузки данных (например, при обмене),
// проверки уникальности обычно отключают, чтобы не нарушать перенос данных.
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
// Проверяем заполненность реквизита. Если он пустой, уникальность контролировать не нужно.
Если Не ЗначениеЗаполнено(УникальныйКод) Тогда
Возврат;
КонецЕсли;
// Выполняем запрос для поиска дублирующих значений
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Товары.Ссылка КАК Ссылка,
| Товары.Наименование КАК Наименование
|ИЗ
| Справочник.Товары КАК Товары
|ГДЕ
| Товары.УникальныйКод = &УникальныйКод
| И Товары.Ссылка <> &ТекущаяСсылка";
Запрос.УстановитьПараметр("УникальныйКод", УникальныйКод);
Запрос.УстановитьПараметр("ТекущаяСсылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
Отказ = Истина;
// Формируем понятное сообщение для пользователя
ТекстСообщения = СтрШаблон(
НСтр("ru = 'Значение реквизита ""Уникальный код"" (%1) уже используется в элементе ""%2""!'"),
УникальныйКод,
Выборка.Наименование
);
ОбщегоНазначенияКлиентСервер.СообщитьПользователю(
ТекстСообщения,
ЭтотОбъект,
"УникальныйКод"
);
КонецЕсли;
КонецПроцедуры
Проанализируем этот код по шагам. Самым важным моментом здесь является условие Товары.Ссылка <> &ТекущаяСсылка в тексте запроса. Оно необходимо для того, чтобы при перезаписи уже существующего элемента система не находила саму себя и не выдавала ложную ошибку дублирования данных.
Представим ситуацию: два пользователя одновременно открыли новые карточки товаров, заполнили один и тот же уникальный код и практически в одну секунду нажали кнопку "Записать".
Давайте разберем по шагам, что произойдет в системе без дополнительной защиты:
ПередЗаписью.Чтобы исключить возникновение такой гонки данных (Race Condition), нам необходимо заблокировать проверяемый ресурс на время выполнения транзакции с помощью механизма управляемых блокировок. Посмотрим на пример реализации такой блокировки внутри транзакции записи:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
Если Не ЗначениеЗаполнено(УникальныйКод) Тогда
Возврат;
КонецЕсли;
// Устанавливаем исключительную блокировку на комбинацию данных
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Товары");
ЭлементБлокировки.УстановитьЗначение("УникальныйКод", УникальныйКод);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Попытка
Блокировка.Заблокировать();
Исключение
Отказ = Истина;
ВызватьИсключение НСтр("ru = 'Не удалось заблокировать данные для проверки уникальности. Попробуйте еще раз.'");
КонецПопытки;
// После успешной блокировки выполняем стандартный проверочный запрос...
// (Код запроса аналогичен Способу 1)
КонецПроцедуры
Важное примечание: Использовать объект БлокировкаДанных для произвольного реквизита справочника напрямую можно только в том случае, если этот реквизит входит в состав индексов (например, является кодом или проиндексирован). В противном случае СУБД будет вынуждена заблокировать всю таблицу справочника, что резко снизит параллельную работу пользователей.
Многие разработчики считают использование регистров сведений для контроля уникальности избыточным решением. Однако в высоконагруженных системах это самый надежный и производительный способ.
Рассмотрим подробнее преимущества использования независимого регистра сведений:
УникальныйКод), платформа 1С на уровне базы данных создаст уникальный индекс. Сама СУБД физически не позволит записать туда дублирующееся значение.Для реализации этого метода создадим регистр сведений со следующей структурой:
ЗначениеРеквизита (Тип: Строка, Число и др.)ОбъектСсылка (Тип: ЛюбаяСсылка)В модуле объекта справочника при записи мы будем регистрировать значение в этом регистре:
Процедура ПриЗаписи(Отказ)
Если ОбменДанными.Загрузка Тогда
Возврат;
КонецЕсли;
Если Не ЗначениеЗаполнено(УникальныйКод) Тогда
Возврат;
КонецЕсли;
// Записываем значение в регистр сведений для фиксации уникальности
МенеджерЗаписи = РегистрыСведений.ОчередьУникальныхКодов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ЗначениеРеквизита = УникальныйКод;
МенеджерЗаписи.ОбъектСсылка = Ссылка;
Попытка
// Метод Записать(Истина) вызовет ошибку на уровне СУБД,
// если такое измерение уже существует в базе данных
МенеджерЗаписи.Записать(Истина);
Исключение
Отказ = Истина;
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = НСтр("ru = 'Данный уникальный код уже зарегистрирован за другим объектом!'");
Сообщение.Поле = "УникальныйКод";
Сообщение.УстановитьОбъект(ЭтотОбъект);
Сообщение.Сообщить();
КонецПопытки;
КонецПроцедуры
Разработчику важно понимать разницу между интерактивной валидацией данных и системным контролем целостности БД.
ОбработкаПроверкиЗаполнения (в модуле объекта): Этот обработчик предназначен исключительно для интерактивной работы пользователя в формах. Если пользователь заполняет форму на клиенте и допускает ошибку, система мягко подсветит поле красным цветом и выдаст красивое сообщение, привязанное к конкретному элементу. Однако, если объект создается программно, этот обработчик по умолчанию не вызывается.
ПередЗаписью (в модуле объекта): Срабатывает всегда — и при интерактивной, и при программной записи. Это идеальное место для жесткого контроля "последней линии обороны", гарантирующего, что дубликат не попадет в базу данных ни при каких обстоятельствах.
Рекомендуется совмещать оба подхода: базовую проверку для интерфейса делать в ОбработкаПроверкиЗаполнения, а жесткий контроль с вызовом исключений дублировать непосредственно перед записью объекта в базу данных. Для наглядного исследования порядка вызова этих процедур можно использовать обработку для анализа последовательности событий платформы.
Если вы выбрали способ проверки уникальности с помощью обычного запроса в ПередЗаписью, критически важно правильно настроить свойства метаданных проверяемого реквизита.
В свойствах реквизита найдите параметр "Индексировать". Платформа предлагает три режима работы:
Установка признака "Индексировать" для проверяемого реквизита — обязательное условие стабильной и быстрой работы вашей информационной системы при росте объема базы данных. При критическом росте количества накопленных записей в 1С:ERP для сокращения размера информационной базы используется многопоточная свёртка 1С:ERP.