Как обеспечить уникальность элемента справочника 1С по сочетанию нескольких полей?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки конфигураций на платформе 1С:Предприятие часто возникает задача контроля уникальности данных. Стандартные механизмы платформы позволяют автоматически проверять уникальность только для одиночных полей — Код или Наименование. Но что делать, если бизнес-логика требует уникальности сочетания нескольких реквизитов? Классический пример — адресный справочник, где сочетание полей Улица + Дом + Квартира должно быть строго уникальным во всей базе данных.

Если не настроить проверку вовремя, пользователи неизбежно начнут вводить дублирующиеся данные. В этой статье мы подробно разберем несколько практических подходов к решению этой задачи: от классического запроса перед записью до продвинутых архитектурных методов, гарантирующих стабильность при высокой нагрузке.

Способ 1. Проверка запросом в событии ПередЗаписью модуля объекта

Самый очевидный и часто используемый способ — написать запрос к базе данных непосредственно перед записью нового или измененного элемента справочника. Давайте разберем по шагам, как это реализовать правильно, чтобы не допустить типовых ошибок начинающих разработчиков.

Важное правило: проверку необходимо выполнять строго в Модуле объекта справочника в обработчике события ПередЗаписью, а не в модуле формы. Если написать проверку на форме, то при программном создании элементов (через обработки, загрузки или веб-сервисы) проверка просто не сработает, и база наполнится дублями.

Также перед выполнением запроса обязательно нужно проверять свойство ОбменДанными.Загрузка. Если идет загрузка данных (например, через РИБ), проверку следует пропустить, чтобы не блокировать процесс обмена.

Давайте проанализируем пример кода, который решает эту задачу:


Процедура ПередЗаписью(Отказ)
    
    // Исключаем проверку, если идет загрузка данных
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |	1 КАК Поле
        |ИЗ
        |	Справочник.Адреса КАК Спр
        |ГДЕ
        |	Спр.Улица = &Улица
        |	И Спр.Дом = &Дом
        |	И Спр.Квартира = &Квартира
        |	И Спр.Ссылка <> &Ссылка"; // Исключаем проверку самого себя при редактировании
        
    Запрос.УстановитьПараметр("Улица", Улица);
    Запрос.УстановитьПараметр("Дом", Дом);
    Запрос.УстановитьПараметр("Квартира", Квартира);
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    
    РезультатЗапроса = Запрос.Выполнить();
    
    Если Не РезультатЗапроса.Пустой() Тогда
        Отказ = Истина;
        Сообщение = Новый СообщениеПользователю;
        Сообщение.Текст = "Элемент с таким сочетанием полей (Улица, Дом, Квартира) уже существует!";
        Сообщение.Поле = "Улица"; // Подсвечиваем поле на форме
        Сообщение.Сообщить();
    КонецЕсли;
    
КонецПроцедуры

Обратите внимание на конструкцию Спр.Ссылка <> &Ссылка. Она критически важна! Если пользователь откроет уже существующий элемент, изменит в нем какое-то другое поле (например, примечание) и нажмет кнопку «Записать», запрос без этого условия найдет этот же самый элемент в базе и заблокирует запись, посчитав его дублем.

Важнейшее требование к оптимизации СУБД: реквизиты, по которым выполняется проверка (в нашем случае Улица, Дом, Квартира), должны иметь свойство «Индексировать» (или «Индексировать с дополнительным упорядочиванием») в конфигураторе. Без индексов СУБД при каждой записи элемента будет производить полное сканирование (Full Scan) таблицы справочника. На большой базе данных это приведет к катастрофическому падению производительности и зависаниям.

Проблема параллельной работы (Race Condition) и транзакционные блокировки

Описанный выше классический метод запроса имеет одну уязвимость, о которой редко задумываются начинающие разработчики. Это проблема параллельной работы пользователей.

Представим ситуацию: два оператора одновременно вводят адрес «ул. Мира, д. 12, кв. 15» и практически в одну секунду нажимают «Записать». Происходит следующее:

  1. Транзакция первого пользователя выполняет запрос проверки — дублей в базе еще нет.
  2. Транзакция второго пользователя в этот же миг выполняет свой запрос проверки — дублей всё еще нет, так как первая транзакция еще не зафиксирована (не завершена).
  3. Обе транзакции успешно записывают данные в базу.
  4. В вашей базе данных появляется дубль, несмотря на наличие программного кода проверки.

Чтобы избежать этой проблемы, перед выполнением проверочного запроса необходимо устанавливать управляемую блокировку на комбинацию проверяемых полей. Сделать это в модуле объекта можно следующим образом:


Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Адреса");
ЭлементБлокировки.УстановитьПараметр("Улица", Улица);
ЭлементБлокировки.УстановитьПараметр("Дом", Дом);
ЭлементБлокировки.УстановитьПараметр("Квартира", Квартира);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

Управляемая блокировка гарантирует, что вторая транзакция будет ожидать завершения первой, после чего её проверочный запрос честно увидит уже записанный первый элемент и выдаст пользователю ошибку уникальности.

Способ 2. Использование стандартного контроля уникальности Кода (Low-Code подход)

Платформа 1С:Предприятие обладает встроенным, крайне производительным механизмом контроля уникальности на уровне СУБД. Он работает для стандартного реквизита Код. Мы можем использовать этот механизм для решения нашей задачи без написания сложных запросов и ручных блокировок.

Давайте разберем алгоритм настройки этого способа:

  1. В свойствах справочника в конфигураторе находим вкладку «Данные».
  2. Устанавливаем длину кода достаточной для хранения склейки наших значений (например, 150 символов).
  3. Тип кода устанавливаем как Строка.
  4. Включаем свойство «Контроль уникальности» кода.
  5. В модуле объекта справочника пишем простой обработчик в событии ПередЗаписью:

Процедура ПередЗаписью(Отказ)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Формируем уникальный символьный код из ключевых полей
    Код = СокрЛП(Улица) + "_" + СокрЛП(Дом) + "_" + СокрЛП(Квартира);
    
КонецПроцедуры

Преимущества данного метода: это невероятно надежно и быстро. Платформа 1С самостоятельно берет на себя транзакционные блокировки и контроль уникальности поля Код на уровне СУБД. Если пользователь попытается записать дубль, платформа выбросит стандартную ошибку «Код не уникален». Нам не нужно думать о параллельных транзакциях.

Недостатки: Код элемента становится техническим полем. Если пользователям вашей системы критически важно видеть красивые автогенерируемые числовые коды (например, «000001», «000002»), этот способ вам не подойдет.

Способ 3. Использование независимого регистра сведений (Профессиональный архитектурный подход)

Для высоконагруженных систем правильным архитектурным решением является вынесение ключевых полей уникальности в физически отдельную таблицу — Регистр сведений (назовем его, к примеру, УникальныеАдреса).

Настроим структуру регистра сведений следующим образом:

При записи элемента справочника в транзакции мы будем делать запись в этот регистр сведений. Поскольку измерения регистра сведений по определению уникальны (платформа физически не позволит записать две строки с одинаковым набором измерений), база данных автоматически заблокирует создание дубликата на уровне физической таблицы СУБД.

Давайте посмотрим на программную реализацию в модуле объекта нашего справочника:


Процедура ПередЗаписью(Отказ)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Если элемент новый, ссылка еще не создана. Получим ее заранее.
    Если ЭтоНовый() Тогда
        УстановитьСсылкуНового(Справочники.Адреса.ПолучитьСсылку());
    КонецЕсли;
    
КонецПроцедуры

Процедура ПриЗаписи(Отказ)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Записываем данные в контролирующий регистр сведений
    НаборЗаписей = РегистрыСведений.УникальныеАдреса.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.Улица.Установить(Улица);
    НаборЗаписей.Отбор.Дом.Установить(Дом);
    НаборЗаписей.Отбор.Квартира.Установить(Квартира);
    
    НоваяЗапись = НаборЗаписей.Добавить();
    НоваяЗапись.Улица = Улица;
    НоваяЗапись.Дом = Дом;
    НоваяЗапись.Квартира = Квартира;
    НоваяЗапись.АдресСсылка = Ссылка;
    
    Попытка
        НаборЗаписей.Записать(Истина); // Замещение существующих записей
    Исключение
        Отказ = Истина;
        Сообщение = Новый СообщениеПользователю;
        Сообщение.Текст = "Внимание! Такой адрес уже зарегистрирован в системе!";
        Сообщение.Сообщить();
    КонецПопытки;
    
КонецПроцедуры

Этот подход признан наиболее масштабируемым и надежным в крупных ERP-системах, так как поиск по индексу регистра сведений выполняется СУБД практически мгновенно, а механизмы платформы по умолчанию исключают дублирование измерений регистра.

Способ 4. Метод хеширования данных (Для большого количества длинных полей)

Рассмотрим специфическую ситуацию: вам нужно контролировать уникальность не по 3 полям, а, например, по 10-15 различным текстовым реквизитам, некоторые из которых могут быть очень длинными. Проводить индексацию такого объема текстовых полей крайне не рекомендуется — это перегрузит СУБД, а СУБД SQL имеет ограничения на максимальную длину индекса строки.

В этом случае на помощь приходит механизм хеширования данных. Мы можем создать в справочнике один единственный реквизит ХешКлюч (Строка, длина 32 или 40 символов) и сделать его индексируемым.

При записи элемента мы будем собирать все наши 15 полей в одну большую строку, вычислять ее уникальный MD5-хеш и записывать в это поле. Поиск дублей будет происходить только по одному этому полю, что работает молниеносно.

Давайте посмотрим, как использовать объект ХешированиеДанных для этой задачи:


Процедура ПередЗаписью(Отказ)
    
    Если ОбменДанными.Загрузка Тогда
        Возврат;
    КонецЕсли;
    
    // Создаем объект хеширования по алгоритму MD5
    ХешОбъект = Новый ХешированиеДанных(ХешФункция.MD5);
    
    // Добавляем все проверяемые поля в расчет хеша
    ХешОбъект.Добавить(Строка(Улица));
    ХешОбъект.Добавить(Строка(Дом));
    ХешОбъект.Добавить(Строка(Квартира));
    // Сюда можно добавить еще хоть 20 полей...
    
    // Получаем строковое представление хеш-суммы
    ХешКлюч = Строка(ХешОбъект.ХешСумма);
    
    // Теперь выполняем быстрый запрос по одному индексированному полю ХешКлюч
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ ПЕРВЫЕ 1
        |	1 КАК Поле
        |ИЗ
        |	Справочник.Адреса КАК Спр
        |ГДЕ
        |	Спр.ХешКлюч = &ХешКлюч
        |	И Спр.Ссылка <> &Ссылка";
        
    Запрос.УстановитьПараметр("ХешКлюч", ХешКлюч);
    Запрос.УстановитьПараметр("Ссылка", Ссылка);
    
    Если Не Запрос.Выполнить().Пустой() Тогда
        Отказ = Истина;
        ВызватьИсключение "Обнаружен дубликат элемента по совокупности ключевых полей!";
    КонецЕсли;
    
КонецПроцедуры

Метод хеширования элегантно решает проблему производительности при работе со сложными составными ключами уникальности, минимизируя нагрузку на дисковую подсистему сервера баз данных.

Резюме

Мы подробно проанализировали четыре отличных способа решения задачи контроля уникальности. Какой из них выбрать — зависит от ваших задач:

← На главную