Как правильно реализовать строгую уникальность артикулов при записи номенклатуры?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

При разработке конфигураций 1С часто возникает задача обеспечения уникальности определенных реквизитов справочников. Классический пример — уникальность артикула в справочнике "Номенклатура". Несмотря на кажущуюся простоту, реализация такой проверки таит в себе множество подводных камней: от неправильного выбора события до блокировок и ошибок при редактировании уже записанных элементов.

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

Анализ проблемы: почему возникает ошибка при создании нового элемента

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

Однако автор темы столкнулся с ситуацией, когда при создании нового товара он якобы обнаруживается запросом как уже существующий, либо возникают сложности с идентификацией "свой/чужой".

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


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

В чем здесь фундаментальная проблема? Дело в том, что мы пытаемся фильтровать результаты после выполнения запроса, перебором выборки. Это неоптимально и усложняет код. Проблема усугубляется непониманием того, в какой момент у объекта появляется Ссылка и когда она попадает в базу данных.

  1. Если элемент новый: метод ЭтоНовый() возвращает Истину. У объекта в памяти может быть назначен уникальный идентификатор (Ссылка), но в таблицах базы данных этой записи еще нет (до момента завершения транзакции записи).
  2. Если элемент существующий: запись в базе есть. Запрос по артикулу обязательно найдет этот же самый элемент.

Правильное построение запроса: исключаем "самих себя"

Самый надежный способ проверки уникальности — отсекать текущий объект прямо в тексте запроса. Нам не нужны сложные циклы Пока Выборка.Следующий(). Нам достаточно знать, есть ли в базе хотя бы одна другая запись с таким же артикулом.

Давайте составим правильный текст запроса. Мы будем искать элементы, у которых артикул совпадает с проверяемым, но ссылка не равна ссылке проверяемого объекта.


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ ПЕРВЫЕ 1
    |	Номенклатура.Ссылка
    |ИЗ
    |	Справочник.Номенклатура КАК Номенклатура
    |ГДЕ
    |	Номенклатура.Артикул = &Артикул
    |	И Номенклатура.Ссылка <> &Ссылка";

Почему это работает идеально?

Выбор события: ОбработкаПроверкиЗаполнения

Автор темы пытался использовать события записи (ПередЗаписью, ПриЗаписи). Хотя технически проверку можно выполнить и там, методологически верным в платформе 1С:Предприятие 8.3 является использование обработчика ОбработкаПроверкиЗаполнения.

Это событие предназначено специально для контроля корректности введенных данных. Его преимущества:

  1. Оно вызывается платформой автоматически перед записью.
  2. Позволяет накопить ошибки. Вы можете сообщить о неуникальном артикуле и одновременно о незаполненном весе, не прерывая проверку на первой ошибке.
  3. Механизм СообщениеПользователю позволяет привязать ошибку конкретно к полю "Артикул" на форме.

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


Процедура ОбработкаПроверкиЗаполнения(Отказ, ПроверяемыеРеквизиты)
	
    // Если артикул не изменен или пустой, проверку можно пропустить (зависит от задачи)
    Если Не ЗначениеЗаполнено(ЭтотОбъект.Артикул) Тогда
        Возврат;
    КонецЕсли;

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

КонецПроцедуры

Важные технические нюансы

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

1. Индексирование

Поиск по строковому полю Артикул может быть очень медленным, если в базе сотни тысяч товаров. Обязательно зайдите в свойства реквизита "Артикул" в конфигураторе и установите свойство Индексировать (или "Индексировать с доп. упорядочиванием"). Это ускорит проверку в разы.

2. Блокировки (Race Condition)

Представьте ситуацию: два менеджера одновременно создают товар с артикулом "A-100". Оба нажимают "Записать" в одну и ту же миллисекунду.
1. Процесс Менеджера 1 делает запрос: "Есть такой артикул?". Ответ базы: "Нет".
2. Процесс Менеджера 2 делает запрос: "Есть такой артикул?". Ответ базы: "Нет" (первый еще не успел записать).
3. Оба процесса записывают данные. В итоге — дубли.

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

Пример установки блокировки:


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

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

Резюме

Подводя итог обсуждения, выделим правильный алгоритм действий:

  1. Используйте событие ОбработкаПроверкиЗаполнения в модуле объекта.
  2. В запросе обязательно используйте условие Ссылка <> &Ссылка для корректной работы при изменении существующих элементов.
  3. Не забывайте проиндексировать поле поиска.
  4. Для высоконагруженных систем используйте управляемые блокировки.
← На главную