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