В процессе эксплуатации информационных систем 1С часто возникает классическая проблема «засорения» кодов справочников. На начальном этапе работы пользователи могут вводить коды вручную, смешивая цифры и буквы (например, "А-101", "тест_5", "999-Ф"). Со временем руководство принимает решение перейти на строгую автоматическую нумерацию только цифрами (например, "000001", "000002").
Однако старые карточки товаров изменять нельзя, поскольку на их коды завязаны внешние интеграции, обмены данными и исторические документы. При попытке назначить новый код стандартный механизм платформы 1С спотыкается о буквенно-цифровые значения, выдавая ошибки неуникальности или некорректно определяя следующий свободный номер. В этой статье мы подробно разберем, как решить эту задачу производительно и безопасно для многопользовательской базы данных.
Поскольку поле Код имеет строковый тип данных, стандартная сортировка или функция СУБД МАКСИМУМ(Код) работает по правилам алфавитного (лексикографического) порядка. В таком представлении строка "9" будет считаться больше, чем строка "100", а значение "А-1" окажется «старше» любого числового значения.
Для решения этой проблемы нам необходимо отфильтровать только чисто числовые коды и найти среди них максимальное значение, чтобы затем прибавить к нему единицу. Рассмотрим несколько способов реализации этой задачи — от простых запросов до изменения архитектуры хранения данных.
Если структура базы данных закрыта для изменений и мы не можем добавлять новые реквизиты, нам придется анализировать строки непосредственно в процессе выполнения запроса к СУБД. Нам нужно построить алгоритм, который посимвольно проверяет строку кода и собирает из нее числовое значение.
Ниже представлена готовая функция, которая динамически формирует текст тяжелого, но рабочего запроса. Она использует конструкции ВЫБОР КОГДА и ПОДСТРОКА для посимвольной конвертации строкового кода длиной до 6 символов в число:
Функция ТекстЗапросаИзСтрокиВЧисло(пИмяПоля, пДлинаСтроки, пСимволРазделителя=".")
лТекст="ВЫБОР ";
Для лДлинаЦелойЧасти=0 По пДлинаСтроки Цикл
Если лДлинаЦелойЧасти=пДлинаСтроки Тогда
лТекст=лТекст+"ИНАЧЕ"+Символы.ПС;
Иначе
лТекст=лТекст+"КОГДА ПОДСТРОКА("+пИмяПоля+", "+(лДлинаЦелойЧасти+1)+", 1)="""+пСимволРазделителя+""" ИЛИ ПОДСТРОКА("+пИмяПоля+", "+(лДлинаЦелойЧасти+1)+", 1)="""" ТОГДА "+Символы.ПС;
КонецЕсли;
Если лДлинаЦелойЧасти=0 Тогда лТекст=лТекст+" 0"+Символы.ПС; КонецЕсли;
Для лИнд=1 По лДлинаЦелойЧасти Цикл
лТекст=лТекст+" ВЫБОР "+Символы.ПС;
Для лЦифра=0 По 9 Цикл
лТекст=лТекст+" КОГДА ПОДСТРОКА("+пИмяПоля+", "+лИнд+", 1)="""+лЦифра+""" ТОГДА "+лЦифра+Sимволы.ПС;
КонецЦикла;
лТекст=лТекст+" ИНАЧЕ 0 КОНЕЦ"+?(лИнд=лДлинаЦелойЧасти, "", "*"+Формат(Pow(10, лДлинаЦелойЧасти-лИнд), "ЧРД=.; ЧГ=0")+"+")+Символы.ПС;
КонецЦикла;
Для лИнд=лДлинаЦелойЧасти+2 По пДлинаСтроки Цикл
лТекст=лТекст+"+ ВЫБОР";
Для лЦифра=0 По 9 Цикл
лТекст=лТекст+" КОГДА ПОДСТРОКА("+пИмяПоля+", "+лИнд+", 1)="""+лЦифра+""" ТОГДА "+лЦифра+Символы.ПС;
КонецЦикла;
лТекст=лТекст+" ИНАЧЕ 0 КОНЕЦ"+?(лИнд=лДлинаЦелойЧасти, "", "/"+Формат(Pow(10, лИнд-лДлинаЦелойЧасти-1), "ЧРД=.; ЧГ=0"))+Символы.ПС;
КонецЦикла;
КонецЦикла;
лТекст=лТекст+"КОНЕЦ";
Возврат лТекст;
КонецФункции
Используя эту вспомогательную функцию, мы можем составить следующий запрос для получения максимального значения:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(" + ТекстЗапросаИзСтрокиВЧисло("Номенклатура.Код", 6) + ") КАК МаксКод
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| НЕ Номенклатура.ЭтоГруппа";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
СледующийКод = Выборка.МаксКод + 1;
Сообщить("Новый свободный код: " + Формат(СледующийКод, "ЧГ=0"));
КонецЕсли;
Важное предупреждение: Данный метод выполняет так называемый Full Scan (полное сканирование таблицы СУБД). Базе данных приходится прочитать абсолютно все записи справочника Номенклатура, применить к каждой строке сложные математические вычисления и только потом вычислить максимум. На базах с объемом номенклатуры более 10 000 элементов этот запрос начнет вызывать серьезные задержки («фризы») интерфейса при создании новых карточек.
Если мы знаем, что наши новые «чистые» цифровые коды всегда формируются по определенному шаблону (например, они всегда начинаются с ведущих нулей или имеют фиксированную длину), мы можем использовать индексированный поиск.
СУБД хранит строковый индекс поля Код в упорядоченном виде. Если наши коды имеют вид "000015", "000016" и так далее, то они будут сгруппированы вместе. Мы можем применить оператор ПОДОБНО для отсечения буквенных кодов:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Код ПОДОБНО ""[0-9][0-9][0-9][0-9][0-9][0-9]""
|УПОРЯДОЧИТЬ ПО
| Код УБЫВ";
РезультатЗапроса = Запрос.Выполнить();
// Далее преобразуем полученную строку в число и увеличиваем на 1
В этом примере конструкция ПОДОБНО "[0-9]..." указывает системе выбирать только те коды, которые состоят исключительно из цифр заданной длины. СУБД использует индекс, быстро находит максимальное строковое значение, удовлетворяющее шаблону, и возвращает результат практически мгновенно.
Самым надежным и производительным решением в промышленной разработке является разделение строкового представления кода и его числовой сущности для целей сортировки.
Разберем этот вариант по шагам:
Номенклатура добавим новый реквизит с именем КодЧислом (тип данных: Число, длина 10, точность 0).ПередЗаписью для справочника номенклатуры (или пропишем код непосредственно в модуле объекта справочника):
Процедура ПередЗаписьюНоменклатуры(Источник, Отказ) Экспорт
// Пытаемся преобразовать текстовый код в числовое значение
Попытка
Источник.КодЧислом = Число(Источник.Код);
Исключение
// Если код содержит буквы, записываем 0, чтобы исключить его из расчетов
Источник.КодЧислом = 0;
КонецПопытки;
КонецПроцедуры
Теперь поиск максимального значения становится тривиальной и мгновенной операцией, которая выполняется по индексу числового поля:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(Номенклатура.КодЧислом) КАК МаксимальныйКод
|ИЗ
| Справочник.Номенклатура КАК Номенклатура";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Если Выборка.Следующий() Тогда
НовыйЧисловойКод = Выборка.МаксимальныйКод + 1;
КонецЕсли;
Если в системе одновременно работают несколько операторов, то при получении кода простым запросом обязательно возникнет ситуация race condition (состояние гонки).
Представим ситуацию: Пользователь А и Пользователь Б одновременно открывают форму создания товара. Система для обоих выполняет запрос, находит максимальный код (например, 100) и предлагает обоим код 101. Тот, кто нажмет кнопку «Записать» первым, займет этот код. Второй пользователь получит ошибку неуникальности кода при записи.
Чтобы избежать этой проблемы, необходимо использовать механизм управляемых блокировок. Мы должны временно заблокировать процесс чтения и записи номеров для других пользователей, пока текущая транзакция не завершится.
Для реализации этого механизма лучше всего использовать независимый Регистр сведений (например, НумерацияНоменклатуры) с одним ресурсом ПоследнийНомер. Схема работы будет выглядеть следующим образом:
Функция ПолучитьНовыйКодНоменклатуры() Экспорт
НачатьТранзакцию();
Попытка
// Накладываем исключительную управляемую блокировку на запись регистра
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.НумерацияНоменклатуры");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Читаем текущее значение из регистра
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Нумерация.ПоследнийНомер КАК ПоследнийНомер
|ИЗ
| РегистрСведений.НумерацияНоменклатуры КАК Нумерация";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
ТекущийНомер = 0;
Если Выборка.Следующий() Тогда
ТекущийНомер = Выборка.ПоследнийНомер;
КонецЕсли;
НовыйНомер = ТекущийНомер + 1;
// Записываем обновленный номер обратно в регистр
МенеджерЗаписи = РегистрыСведений.НумерацияНоменклатуры.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ПоследнийНомер = НовыйНомер;
МенеджерЗаписи.Записать(Истина);
ЗафиксироватьТранзакцию();
Возврат НовыйНомер;
Исключение
ОтменитьТранзакцию();
ВызватьИсключение "Не удалось получить новый код номенклатуры: " + ОписаниеОшибки();
КонецПопытки;
КонецФункции
При таком подходе другие пользователи, пытающиеся создать карточку товара в ту же секунду, будут ожидать своей очереди на чтение номера. Блокировка гарантирует, что каждый пользователь получит строго уникальное значение.
Если мы хотим полностью уйти от написания собственных велосипедов и доверить нумерацию платформе 1С, лучшим выходом будет внедрение нового **префикса** для всей создаваемой номенклатуры.
Например, мы решаем, что все новые товары будут иметь префикс "Н" (например, "Н-000001", "Н-000002").
Для реализации этого метода:
УстановитьНовыйКод("Н-").Если после ручных исправлений кодов пользователями автонумерация начала выдавать сбои или путать диапазоны, администратору необходимо сбросить кэш номеров, выполнив в обработке команду:
ОбновитьНумерациюОбъектов(Метаданные.Справочники.Номенклатура);
Эта команда заставляет платформу сбросить кэш номеров в памяти, пересчитать физическую таблицу базы данных и начать генерацию кодов с корректного максимального значения.