При разработке конфигураций на платформе 1С:Предприятие 8 часто возникает задача создания альтернативной нумерации. Это может быть связано с требованиями законодательства для бюджетных учреждений, спецификой учета договоров или необходимостью нумеровать только определенные виды документов (например, чеки ККМ с определенным набором услуг). Основная сложность здесь заключается в обеспечении уникальности номера в многопользовательской среде, где несколько кассиров или операторов могут записывать документы практически одновременно.
В этой статье мы подробно разберем, почему стандартные методы «в лоб» приводят к задвоению номеров, и как правильно спроектировать надежный механизм нумерации с использованием механизмов платформы.
Рассмотрим стандартную ситуацию, с которой столкнулся автор темы. Программист реализовал логику в подписке на событие ПередЗаписью. Алгоритм был следующим: найти в базе последний документ, взять его номер, прибавить единицу и присвоить новому объекту. Проблема в том, что в системе с несколькими рабочими местами возникает так называемое состояние гонки (race condition).
Проанализируем пошагово, что происходит на уровне сервера приложений и СУБД:
В итоге мы получаем дубль. Обычное чтение запросом не блокирует данные для других пользователей, поэтому нам необходим механизм управляемых блокировок.
Для хранения последнего выданного номера можно использовать разные объекты. Рассмотрим основные варианты:
1. Константа. Это самый простой объект. С точки зрения производительности в современных версиях платформы (начиная с 8.2.14) каждая константа хранится в отдельной таблице СУБД, поэтому блокировка одной константы не мешает работе с другими. Однако константа не позволяет гибко масштабировать систему (например, если завтра потребуется нумерация в разрезе организаций или касс).
2. Регистр сведений (непериодический, независимый). Это наиболее гибкий и профессиональный подход. Мы можем создать регистр НумерацияДоговоров с одним ресурсом Номер (тип Число). Если потребуется разделение нумерации, мы просто добавим измерение (например, Организация).
Проанализируем результаты теста производительности из обсуждения. При выполнении 2000 операций записи константы отрабатывают быстрее, чем регистр сведений, однако в реальных условиях (100-500 чеков в день) эта разница в миллисекундах будет абсолютно незаметна для пользователя. Регистр сведений выигрывает за счет своей архитектурной гибкости.
Чтобы избежать задвоений, мы должны гарантировать, что в момент чтения последнего номера и его инкремента никто другой не сможет прочитать это же значение. Для этого мы будем использовать объект БлокировкаДанных.
Рассмотрим пример кода, который следует разместить в модуле объекта документа или в специализированном общем модуле. Мы будем использовать событие ПередЗаписью, предварительно проверив, не установлен ли номер ранее, чтобы избежать его изменения при простой перезаписи документа.
Процедура ПрисвоитьСвойНомерДоговора(Отказ, РежимЗаписи, РежимПроведения)
// Если номер уже заполнен, ничего не делаем
Если ЗначениеЗаполнено(ЭтотОбъект.НомерДоговора) Тогда
Возврат;
КонецЕсли;
// Проверяем условия, нужно ли вообще нумеровать этот документ
НужноНумеровать = Ложь;
Для Каждого Стр Из ЭтотОбъект.Товары Цикл
Если Стр.Номенклатура.ВидНоменклатуры = Справочники.ВидыНоменклатуры.УслугаДоговорная Тогда
НужноНумеровать = Истина;
Прервать;
КонецЕсли;
КонецЦикла;
Если Не НужноНумеровать Тогда
Возврат;
КонецЕсли;
// Начинаем работу с блокировкой
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ПоследниеНомераДоговоров");
// Если есть измерения (например, Организация), их нужно указать для точечной блокировки
// ЭлементБлокировки.УстановитьЗначение("Организация", ЭтотОбъект.Организация);
Блокировка.Заблокировать();
// Теперь внутри транзакции читаем последнее значение
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПоследниеНомераДоговоров.Номер КАК ТекущийНомер
|ИЗ
| РегистрСведений.ПоследниеНомераДоговоров КАК ПоследниеНомераДоговоров";
// ВАЖНО: Если регистр пустой, это будет первый номер
РезультатЗапроса = Запрос.Выполнить();
НовыйНомер = 1;
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Выборка.Следующий();
НовыйНомер = Выборка.ТекущийНомер + 1;
КонецЕсли;
// Записываем новый номер в регистр (обновляем счетчик)
МенеджерЗаписи = РегистрыСведений.ПоследниеНомераДоговоров.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Номер = НовыйНомер;
// МенеджерЗаписи.Организация = ЭтотОбъект.Организация; // Если есть измерения
МенеджерЗаписи.Записать();
// Присваиваем номер документу
ЭтотОбъект.НомерДоговора = НовыйНомер;
КонецПроцедуры
Проанализируем, в какой момент жизненного цикла документа лучше вызывать этот код. Существует несколько вариантов, и у каждого есть свои нюансы:
ДополнительныеСвойства.Свойство("ЭтоУстановкаНомера") или проверка заполненности реквизита присутствует, чтобы не менять номер при каждом сохранении.Номер. Для произвольного реквизита (как в нашем случае НомерДоговора) он автоматически не вызывается. Его можно использовать, если вы вызываете метод ЭтотОбъект.УстановитьНовыйНомер() принудительно, но это добавит сложности.Выясним важную деталь, упомянутую в сообщении 34: почему могут возникать ошибки при оплате картой. Фискальный регистратор (ФР) — устройство медленное. Печать чека может занимать 5-10 секунд. Если вы открыли транзакцию, установили блокировку на регистр нумерации, а затем начали печатать чек на ФР, то вся таблица нумератора будет заблокирована до тех пор, пока принтер не закончит работу.
В это время другой кассир на другой кассе попытается пробить чек, обратится к тому же регистру и будет ждать (Timeout). Если ожидание превысит 20 секунд, вторая касса выдаст ошибку.
Рекомендация: старайтесь делать транзакцию получения номера максимально короткой. Если бизнес-логика позволяет, лучше сначала записать документ, а номер присваивать в самый последний момент перед отправкой команды на печать, либо использовать механизм ПередЗаписьюНаСервере в модуле формы, чтобы получить номер до начала основной тяжелой транзакции записи документа.
Подведем итоги нашего анализа. Для создания надежной нумерации «своего поля» выполним следующие шаги:
Номер.БлокировкаДанных перед чтением и обновлением номера в регистре.ПередЗаписью модуля объекта.Если ЗначениеЗаполнено(НомерДоговора) Тогда Возврат;, чтобы номер не «перескакивал» при редактировании документа.Такой подход гарантирует строгую последовательность номеров и отсутствие дублей даже при очень интенсивной работе большого количества пользователей в одной базе данных.