В процессе разработки на платформе 1С часто возникает задача создания кастомной системы нумерации, которая не укладывается в стандартные механизмы платформы. Типичный пример — номер заказа, который должен содержать в себе префикс в виде года и месяца (ГГММ) и порядковый номер, обнуляющийся в начале каждого месяца (например, 25010101). При этом необходимо обеспечить абсолютную уникальность номера, исключив влияние "человеческого фактора" (ручной правки) и параллельной работы нескольких пользователей.
В этой статье мы подробно разберем, почему "запрос в цикле" — это не самое удачное решение, и проанализируем профессиональные подходы к реализации данной задачи, включая использование транзакционных блокировок и регистров сведений.
Стандартная автонумерация 1С работает достаточно надежно, но она имеет свои особенности. Платформа 8.1 и более поздние версии используют кэширование номеров для повышения производительности. Однако, если бизнес-логика требует сложного формата (ГГММ + счетчик) и при этом допускается ручное редактирование номеров, стандартный механизм может начать выдавать дубли или пропускать номера. Рассмотрим ситуацию, когда пользователь вручную установил номер 25010110, в то время как счетчик в системе был на значении 25010105. При создании следующего документа система может попытаться присвоить уже занятый номер 25010110, что приведет к ошибке уникальности.
Один из самых простых и популярных способов — хранение текущего значения счетчика в Константе. Чтобы избежать ситуации, когда два пользователя одновременно считывают одно и то же значение константы, мы должны использовать объект БлокировкаДанных. Это гарантирует, что пока один процесс увеличивает номер, остальные будут ждать завершения транзакции.
Разберем алгоритм по шагам:
Пример программной реализации в модуле объекта:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если ЭтоНовый() И НЕ ЗначениеЗаполнено(Номер) Тогда
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Константа.ПоследнийНомерЗаказа");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
НачатьТранзакцию();
Попытка
Блокировка.Заблокировать();
ТекущийСчетчик = Константы.ПоследнийНомерЗаказа.Получить();
НовыйСчетчик = ТекущийСчетчик + 1;
// Формируем префикс ГГММ
Префикс = Формат(ТекущаяДата(), "ДФ=yyMM");
ПредполагаемыйНомер = Префикс + Формат(НовыйСчетчик, "ЧЦ=4; ЧГ=0; ЧВН=");
// Проверка на случай, если кто-то ввел номер вручную
Пока Истина Цикл
Запрос = Новый Запрос("ВЫБРАТЬ ПЕРВЫЕ 1 Ссылка ИЗ Документ.ЗаказПокупателя ГДЕ Номер = &Номер");
Запрос.УстановитьПараметр("Номер", ПредполагаемыйНомер);
Если Запрос.Выполнить().Пустой() Тогда
Прервать;
Иначе
НовыйСчетчик = Новый Счик + 1;
ПредполагаемыйНомер = Префикс + Формат(НовыйСчетчик, "ЧЦ=4; ЧГ=0; ЧВН=");
КонецЕсли;
КонецЦикла;
Номер = ПредполагаемыйНомер;
Константы.ПоследнийНомерЗаказа.Установить(НовыйСчетчик);
ЗафиксироватьТранзакцию();
Исключение
ОтменитьТранзакцию();
Сообщить("Ошибка при формировании номера: " + ОписаниеОшибки());
Отказ = Истина;
КонецПопытки;
КонецЕсли;
КонецПроцедуры
Использование константы не всегда удобно, если нужно хранить историю или разделять нумерацию по организациям, годам или месяцам. Более гибкий подход — использование Регистра Сведений (например, НумерацияДокументов) с измерениями Период (тип Дата, начало месяца) и ТипДокумента. В ресурсе будем хранить ПоследнийНомер.
Преимущества этого метода:
Рассмотрим логику поиска "дырок" в нумерации. Если мы хотим быть максимально аккуратными, мы можем использовать запрос для поиска максимального номера в текущем месяце:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МАКСИМУМ(Заказ.Номер) КАК МаксНомер
|ИЗ
| Документ.ЗаказПокупателя КАК Заказ
|ГДЕ
| Заказ.Дата МЕЖДУ &НачалоМесяца И &КонецМесяца";
Запрос.УстановитьПараметр("НачалоМесяца", НачалоМесяца(ТекущаяДата()));
Запрос.УстановитьПараметр("КонецМесяца", КонецМесяца(ТекущаяДата()));
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() И ЗначениеЗаполнено(Результат.МаксНомер) Тогда
// Извлекаем числовую часть из строки номера и инкрементируем
// Логика обработки строки...
КонецЕсли;
Автор темы предлагал выполнять запрос в цикле до 20 раз, проверяя каждый следующий номер. Проанализируем ситуацию: если в системе работают одновременно 50 операторов и происходит массовая запись заказов, "запрос в цикле" может привести к значительным задержкам. Каждый запрос — это обращение к СУБД. Если номер уже занят, и мы начинаем перебор, транзакция удерживается открытой всё это время. Это классический пример того, как небольшая "заплатка" может превратиться в узкое место производительности (bottleneck) при росте базы.
Вместо этого мы рекомендуем:
БлокировкаДанных в режиме Исключительный гарантирует, что вы — единственный, кто сейчас работает с этим номером.Попытка...Исключение, так как в многопользовательской среде конфликт блокировок — это штатная ситуация, которую нужно корректно обработать, не пугая пользователя системными ошибками.Выясним причину возможных сбоев в старых версиях платформы. В 1С 8.1 механизм ОбновитьНумерациюОбъектов() часто помогал восстановить сбитую последовательность, но он не спасает от дублей, если логика формирования номера вынесена в код.
Проанализируем еще один важный момент: человеческий фактор. Чтобы избежать проблем, когда "кто-то вручную назначил номер 850", рекомендуется:
ТолькоПросмотр или через ПриЧтенииНаСервере).ПередЗаписью, которая будет выдавать предупреждение, если введенный вручную номер уже существует.Подводя итог, наиболее надежным и современным способом для 1С (даже для версии 8.1) является сочетание Регистра сведений для хранения счетчиков и Объекта БлокировкаДанных для обеспечения целостности данных в момент записи. Это позволит вашей системе работать быстро и без ошибок даже при значительном увеличении потока заказов.