При ведении учета в 1С:Предприятие (в частности, в конфигурациях УПП 1.3, КА 1.1 или УТ 10.3) часто возникает задача автоматического присвоения уникального артикула для новых позиций номенклатуры. Стандартно поле Артикул представляет собой текстовую строку, что накладывает определенные ограничения при попытке автоматического расчета следующего номера. Если просто выбирать максимальное значение запросом, система может начать двоить номера или постоянно выдавать один и тот же артикул.
В этой статье мы подробно разберем, почему возникает эта проблема, рассмотрим базовое решение с форума, проанализируем его слабые места и изучим профессиональные подходы к реализации автонумерации с учетом многопользовательской работы и оптимизации производительности СУБД.
Первая и самая распространенная ошибка при разработке автонумерации артикулов — использование функции МАКСИМУМ в запросе по текстовому полю без жесткой фильтрации формата. Рассмотрим, как СУБД выполняет сортировку строковых данных (лексикографическая сортировка):
Номенклатура присутствует хотя бы один артикул, введенный вручную не по шаблону (например, "9", "А-100", "МК-99" или просто строка с пробелом на конце), функция МАКСИМУМ(Артикул) начнет постоянно возвращать именно эту некорректную строку.В результате алгоритм инкремента (прибавления единицы) зависает на одном месте, выдавая пользователям повторяющийся номер (например, "000005").
Чтобы обойти проблему лексикографической сортировки, мы должны выбирать из базы только те артикулы, которые строго соответствуют нашему шаблону (например, две буквы префикса "МК" и 6 цифр номера). Разберем рабочий пример кода, предложенный на форуме.
Для решения этой задачи мы используем оператор ПОДОБНО в запросе 1С, который позволяет отсечь все некорректные и "ручные" артикулы:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
// Выполняем генерацию только для новых элементов
Если ЭтоНовый() Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Номенклатура.Артикул КАК Артикул
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Артикул ПОДОБНО ""МК[0-9][0-9][0-9][0-9][0-9][0-9]""
|УПОРЯДОЧИТЬ ПО
| Артикул УБЫВ";
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
// Извлекаем только числовую часть (последние 6 символов)
НомерЧислом = Число(Прав(Выборка.Артикул, 6)) + 1;
// Форматируем обратно в строку с лидирующими нулями
ЭтотОбъект.Артикул = "МК" + Формат(НомерЧислом, "ЧЦ=6; ЧВН=; ЧГ=");
Иначе
// Если в базе еще нет артикулов по нашему шаблону
ЭтотОбъект.Артикул = "МК000001";
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Проанализируем ключевые моменты этого кода:
ПОДОБНО ""МК[0-9][0-9][0-9][0-9][0-9][0-9]"" гарантирует, что запрос вернет только те артикулы, которые начинаются на "МК" и содержат ровно 6 цифр после префикса. Символы в квадратных скобках [0-9] означают любую одиночную цифру.УПОРЯДОЧИТЬ ПО Артикул УБЫВ совместно с инструкцией ВЫБРАТЬ ПЕРВЫЕ 1 позволяет быстро получить самый свежий (максимальный) артикул.Формат() с параметром ЧВН= (что означает "Члены С Нулями") автоматически дополняет число лидирующими нулями до указанной длины ЧЦ=6. Это избавляет нас от громоздких условий Если НомерАртикула < 10 Тогда ..., которые использовал автор темы в своем исходном коде.Рассмотренный выше базовый вариант отлично работает в локальной базе или при работе одного оператора. Однако в реальной многопользовательской базе (SQL-вариант) он неизбежно приведет к возникновению дублей артикулов.
Рассмотрим ситуацию: Оператор А и Оператор Б одновременно нажимают кнопку "Записать" на двух новых карточках номенклатуры. В обеих транзакциях параллельно выполняется запрос к базе. СУБД считывает максимальный артикул (например, "МК000125") для обоих пользователей. Программа прибавляет единицу, и обе карточки пытаются записаться с артикулом "МК000126". Если в конфигурации не стоит жесткий запрет на дублирование артикулов, база наполнится неуникальными данными. Если запрет стоит — один из пользователей получит критическую ошибку транзакции.
Для предотвращения этой проблемы ("гонки данных") необходимо использовать механизм управляемых блокировок. Мы должны временно заблокировать считываемый диапазон данных до момента завершения записи нашей номенклатуры.
Посмотрим на пример реализации с установкой управляемой блокировки:
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.Номенклатура");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// Блокируем весь справочник на чтение для других транзакций до окончания нашей транзакции
Блокировка.Заблокировать();
Однако блокировка целого справочника Номенклатура — крайне нежелательное действие. Это приведет к тому, что во время создания товара одним пользователем все остальные сотрудники не смогут даже записать изменения в уже существующих карточках номенклатуры. Они будут "висеть" в ожидании блокировки.
Чтобы избежать "Full Table Scan" (полного сканирования огромной таблицы номенклатуры при каждом поиске артикула) и уйти от блокировок справочника, разработчики используют вспомогательный независимый регистр сведений. Назовем его, например, СчетчикиАртикулов.
Структура регистра СчетчикиАртикулов:
Префикс (Тип: Строка, длина 10)ТекущееЗначение (Тип: Число, длина 10)Разберем алгоритм получения нового артикула через данный регистр сведений в транзакции:
Функция ПолучитьНовыйАртикул(Префикс) Экспорт
НачатьТранзакцию();
Попытка
// Устанавливаем управляемую блокировку ТОЛЬКО на одну строку регистра с нашим префиксом
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.СчетчикиАртикулов");
ЭлементБлокировки.УстановитьПараметр("Префикс", Префикс);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Считываем текущее значение счетчика
Отбор = Новый Структура("Префикс", Префикс);
ЗначениеСчетчика = 0;
Выборка = РегистрыСведений.СчетчикиАртикулов.Выбрать(Отбор);
Если Выборка.Следующий() Тогда
ЗначениеСчетчика = Выборка.ТекущееЗначение;
КонецЕсли;
// Увеличиваем счетчик на 1
НовоеЗначение = ЗначениеСчетчика + 1;
// Записываем обновленное значение обратно в регистр счетчиков
МенеджерЗаписи = РегистрыСведений.СчетчикиАртикулов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Префикс = Префикс;
МенеджерЗаписи.ТекущееЗначение = НовоеЗначение;
МенеджерЗаписи.Записать(Истина);
ЗафиксироватьТранзакцию();
// Формируем строковый артикул
Возврат Префикс + Формат(НовоеЗначение, "ЧЦ=6; ЧВН=; ЧГ=");
Исключение
ОтменитьТранзакцию();
ВызватьИсключение "Не удалось сгенерировать уникальный артикул. Повторите попытку. Описание ошибки: " + ОписаниеОшибки();
КонецПопытки;
КонецФункции
Преимущества архитектуры с регистром сведений:
Префикс. Нет необходимости сканировать миллионы строк справочника Номенклатура.Если вам не хочется создавать новые объекты метаданных (регистры сведений) и писать сложный код блокировок, проанализируем еще один элегантный и надежный способ. Платформа 1С:Предприятие уже содержит в себе оптимизированный, высокопроизводительный механизм автонумерации кодов справочников, который решает все проблемы блокировок "из коробки".
Мы можем настроить генерацию артикула на основе стандартного поля Код справочника Номенклатура:
Номенклатура включаем автонумерацию кодов, устанавливаем тип кода "Строка" и задаем нужную длину (например, 8 символов).ПередЗаписью модуля объекта просто транслируем полученный код в поле артикула:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ЭтоНовый() Тогда
// Если код еще не присвоен (присваивается платформой при записи)
// Мы можем временно оставить поле Артикул пустым или заполнить его при УстановкеНовогоКода
КонецЕсли;
КонецПроцедуры
Более правильным местом для такой привязки будет стандартное событие модуля объекта ПриУстановкеНовогоКода. Этот обработчик вызывается платформой в момент генерации нового кода:
Процедура ПриУстановкеНовогоКода(СтандартнаяОбработка, Префикс)
// Добавляем к коду наш собственный префикс, если это необходимо
Префикс = "МК";
// Сразу же дублируем будущее значение кода в артикул
// К моменту записи платформа сформирует код и запишет его в оба реквизита
ЭтотОбъект.Артикул = ЭтотОбъект.Код;
КонецПроцедуры
Использование штатного механизма нумерации кодов — максимально безопасный путь, который гарантирует высокую скорость работы и полное отсутствие конфликтов блокировок СУБД даже при одновременной работе сотен пользователей.
Если вы решаете аналогичную задачу в современных конфигурациях на управляемых формах, настоятельно рекомендуем не изменять типовой код конфигурации напрямую. Все доработки лучше выполнять с помощью механизма расширений.
Создайте расширение конфигурации, добавьте в него справочник Номенклатура и создайте обработчик события модуля объекта ПередЗаписью с аннотацией &После или &Перед. Это позволит сохранить базу данных на полной поддержке и значительно облегчит процесс последующих обновлений.