Как обеспечить уникальную нумерацию объектов, создаваемых внутри транзакции в 1С?

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

В процессе разработки часто возникает задача создания и записи нескольких объектов внутри одной большой транзакции. Классический пример — пакетная загрузка данных. Если у создаваемых объектов, например, элементов справочника, должен быть уникальный номер, который генерируется по принципу "найти максимальный + 1", можно столкнуться с неожиданной проблемой: для всех новых объектов будет присвоен один и тот же номер.

Рассмотрим, почему это происходит и какие существуют правильные подходы для решения этой задачи в среде "1С:Предприятие".

Анализ проблемы: почему запрос возвращает одинаковый номер

Давайте разберем ситуацию. Вы начинаете транзакцию, в цикле создаете новые элементы справочника, и для каждого из них перед записью выполняете запрос к базе данных, чтобы получить максимальный существующий номер. Логика проста:


// Псевдокод, иллюстрирующий проблему
НачатьТранзакцию();

Для Каждого ЭлементДанных Из Коллекция Цикл
    НовыйЭлемент = Справочники.МойСправочник.СоздатьЭлемент();
    
    // Получаем "последний" номер из базы данных
    Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(Т.Номер) КАК МаксНомер ИЗ Справочник.МойСправочник КАК Т");
    Результат = Запрос.Выполнить().Выбрать();
    Результат.Следующий();
    
    // Присваиваем новый номер
    НовыйЭлемент.Номер = Результат.МаксНомер + 1;
    НовыйЭлемент.Записать(); // Запись происходит внутри транзакции
КонецЦикла;

ЗафиксироватьТранзакцию();

Проблема в том, что до момента фиксации транзакции (ЗафиксироватьТранзакцию()), новые, записанные элементы физически не видны для последующих запросов к базе данных из этой же транзакции. С точки зрения СУБД, этих данных еще "не существует" для других. Это стандартное поведение, обеспечиваемое механизмом изоляции транзакций (обычно уровень Read Committed).

Ключевая причина, которая часто усугубляет ситуацию, заключается в вызове проведения документа из внешней транзакции. Например, у вас есть обработка, которая создает и записывает элементы справочников, а затем создает и проводит документ. Проведение документа (вызов метода Записать() с параметром РежимЗаписиДокумента.Проведение) всегда запускает свою собственную, новую транзакцию. Эта "вложенная" транзакция ничего не знает о данных, которые были записаны, но не зафиксированы в "родительской" транзакции. В итоге, если логика нумерации находится внутри модуля проведения, она будет работать с устаревшими данными.

Теперь рассмотрим системные способы решения этой задачи.

Решение 1: Использование управляемых блокировок

Наиболее правильный и рекомендованный платформой "1С:Предприятие" метод для обеспечения целостности данных при параллельной работе — это механизм управляемых блокировок. Он позволяет явно "зарезервировать" определенный срез данных на время выполнения операции, не давая другим сеансам его изменить.

Как это работает по шагам:

  1. Перед циклом создания объектов мы устанавливаем исключительную блокировку на таблицу справочника. Это гарантирует, что ни одна другая транзакция не сможет в этот момент получить максимальный номер и создать свой элемент.
  2. Внутри транзакции мы один раз получаем максимальный номер из базы данных и сохраняем его в локальную переменную.
  3. В цикле мы просто увеличиваем эту переменную на единицу и присваиваем ее новым объектам. Это намного производительнее, чем выполнять запрос в каждой итерации.
  4. Блокировка будет автоматически снята после завершения транзакции (фиксации или отката).

Посмотрим на пример кода:


НачатьТранзакцию();

// 1. Устанавливаем управляемую блокировку
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.МойСправочник");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();

// 2. Один раз получаем максимальный номер
ТекущийНомер = 0;
Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(Т.Номер) КАК МаксНомер ИЗ Справочник.МойСправочник КАК Т");
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
    ТекущийНомер = ?(Результат.МаксНомер = NULL, 0, Результат.МаксНомер);
КонецЕсли;

// 3. В цикле работаем с переменной
Для Каждого ЭлементДанных Из Коллекция Цикл
    ТекущийНомер = ТекущийНомер + 1;
    
    НовыйЭлемент = Справочники.МойСправочник.СоздатьЭлемент();
    НовыйЭлемент.Номер = ТекущийНомер;
    НовыйЭлемент.Записать();
КонецЦикла;

ЗафиксироватьТранзакцию();

Этот подход гарантирует уникальность номеров и является самым надежным решением при высокой нагрузке на систему.

Решение 2: Регистр-нумератор

Еще один распространенный и очень надежный паттерн — создание отдельного объекта для хранения последнего использованного номера. Обычно для этого используют непериодический регистр сведений.

Разберем по шагам:

  1. Создаем регистр сведений, например, "ПоследниеНомераОбъектов".
  2. В измерениях указываем аналитику, в разрезе которой ведется нумерация (например, "ИмяСправочника", "Организация").
  3. В ресурсах создаем поле для хранения номера, например, "ПоследнийНомер" (число).

Алгоритм работы с таким регистром внутри транзакции должен быть следующим:

  1. Обязательно! Установить исключительную управляемую блокировку на запись регистра сведений по нужным измерениям. Это критически важно для предотвращения "гонки состояний", когда два сеанса одновременно читают одно и то же значение.
  2. Прочитать из регистра текущее значение последнего номера.
  3. Увеличить его на 1.
  4. Присвоить новый номер создаваемому объекту.
  5. Записать обновленное значение обратно в регистр нумератора.

Пример кода:


Процедура ПрисвоитьСледующийНомер(НовыйОбъект)
    
    НачатьТранзакцию();
    
    // 1. Блокируем запись в регистре-нумераторе
    Блокировка = Новый БлокировкаДанных;
    ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ПоследниеНомераОбъектов");
    ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
    // Устанавливаем отбор по измерениям, если нумерация ведется в разрезе чего-либо
    ЭлементБлокировки.УстановитьЗначение("ИмяСправочника", "МойСправочник");
    Блокировка.Заблокировать();
    
    // 2. Читаем последнее значение
    МенеджерЗаписи = РегистрыСведений.ПоследниеНомераОбъектов.СоздатьМенеджерЗаписи();
    МенеджерЗаписи.ИмяСправочника = "МойСправочник";
    МенеджерЗаписи.Прочитать();
    
    ПоследнийНомер = МенеджерЗаписи.ПоследнийНомер;
    
    // 3. Увеличиваем и присваиваем
    НовыйНомер = ПоследнийНомер + 1;
    НовыйОбъект.Номер = НовыйНомер;
    
    // 4. Записываем новое значение в регистр
    МенеджерЗаписи.ПоследнийНомер = НовыйНомер;
    МенеджерЗаписи.Записать();
    
    ЗафиксироватьТранзакцию();
    
КонецПроцедуры

Этот метод очень гибок, так как позволяет настраивать сложные схемы нумерации с разными счетчиками для разных условий.

Решение 3: Передача "контекста" в дочернюю транзакцию

Если ваша проблема вызвана именно вложенной транзакцией (например, при проведении документа), то можно изменить архитектуру кода, чтобы избежать повторного запроса к базе данных.

Идея в том, чтобы заранее сгенерировать номера в родительской транзакции и передать их в дочернюю в качестве параметров.

Как это можно реализовать:

  1. В родительской транзакции, перед вызовом проведения документа, определите, сколько новых объектов нужно создать.
  2. Используя один из вышеописанных методов (с блокировкой), получите необходимое количество уникальных номеров и сохраните их, например, в Массив или ТаблицуЗначений.
  3. Передайте эту коллекцию номеров в процедуру проведения документа. Это можно сделать, добавив в объект документа временный реквизит (не сохраняемый в БД) или передав структуру через дополнительные параметры при записи (если вы используете свою, а не типовую процедуру проведения).
  4. В модуле проведения документа код больше не обращается к базе за максимальным номером, а просто берет готовые номера из переданной коллекции.

Этот подход решает проблему изоляции транзакций наиболее прямолинейно, разделяя логику получения номеров и логику их использования.

Итог: Проблема с получением одинаковых номеров внутри транзакции — это не ошибка платформы, а следствие работы механизма изоляции СУБД. Для ее решения следует использовать системные инструменты, такие как управляемые блокировки, или применять надежные архитектурные паттерны, например, регистр-нумератор или явную передачу контекста между транзакциями.

← На главную