В процессе разработки часто возникает задача создания и записи нескольких объектов внутри одной большой транзакции. Классический пример — пакетная загрузка данных. Если у создаваемых объектов, например, элементов справочника, должен быть уникальный номер, который генерируется по принципу "найти максимальный + 1", можно столкнуться с неожиданной проблемой: для всех новых объектов будет присвоен один и тот же номер.
Рассмотрим, почему это происходит и какие существуют правильные подходы для решения этой задачи в среде "1С:Предприятие".
Давайте разберем ситуацию. Вы начинаете транзакцию, в цикле создаете новые элементы справочника, и для каждого из них перед записью выполняете запрос к базе данных, чтобы получить максимальный существующий номер. Логика проста:
// Псевдокод, иллюстрирующий проблему
НачатьТранзакцию();
Для Каждого ЭлементДанных Из Коллекция Цикл
НовыйЭлемент = Справочники.МойСправочник.СоздатьЭлемент();
// Получаем "последний" номер из базы данных
Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(Т.Номер) КАК МаксНомер ИЗ Справочник.МойСправочник КАК Т");
Результат = Запрос.Выполнить().Выбрать();
Результат.Следующий();
// Присваиваем новый номер
НовыйЭлемент.Номер = Результат.МаксНомер + 1;
НовыйЭлемент.Записать(); // Запись происходит внутри транзакции
КонецЦикла;
ЗафиксироватьТранзакцию();
Проблема в том, что до момента фиксации транзакции (ЗафиксироватьТранзакцию()), новые, записанные элементы физически не видны для последующих запросов к базе данных из этой же транзакции. С точки зрения СУБД, этих данных еще "не существует" для других. Это стандартное поведение, обеспечиваемое механизмом изоляции транзакций (обычно уровень Read Committed).
Ключевая причина, которая часто усугубляет ситуацию, заключается в вызове проведения документа из внешней транзакции. Например, у вас есть обработка, которая создает и записывает элементы справочников, а затем создает и проводит документ. Проведение документа (вызов метода Записать() с параметром РежимЗаписиДокумента.Проведение) всегда запускает свою собственную, новую транзакцию. Эта "вложенная" транзакция ничего не знает о данных, которые были записаны, но не зафиксированы в "родительской" транзакции. В итоге, если логика нумерации находится внутри модуля проведения, она будет работать с устаревшими данными.
Теперь рассмотрим системные способы решения этой задачи.
Наиболее правильный и рекомендованный платформой "1С:Предприятие" метод для обеспечения целостности данных при параллельной работе — это механизм управляемых блокировок. Он позволяет явно "зарезервировать" определенный срез данных на время выполнения операции, не давая другим сеансам его изменить.
Как это работает по шагам:
Посмотрим на пример кода:
НачатьТранзакцию();
// 1. Устанавливаем управляемую блокировку
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("Справочник.МойСправочник");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// 2. Один раз получаем максимальный номер
ТекущийНомер = 0;
Запрос = Новый Запрос("ВЫБРАТЬ МАКСИМУМ(Т.Номер) КАК МаксНомер ИЗ Справочник.МойСправочник КАК Т");
Результат = Запрос.Выполнить().Выбрать();
Если Результат.Следующий() Тогда
ТекущийНомер = ?(Результат.МаксНомер = NULL, 0, Результат.МаксНомер);
КонецЕсли;
// 3. В цикле работаем с переменной
Для Каждого ЭлементДанных Из Коллекция Цикл
ТекущийНомер = ТекущийНомер + 1;
НовыйЭлемент = Справочники.МойСправочник.СоздатьЭлемент();
НовыйЭлемент.Номер = ТекущийНомер;
НовыйЭлемент.Записать();
КонецЦикла;
ЗафиксироватьТранзакцию();
Этот подход гарантирует уникальность номеров и является самым надежным решением при высокой нагрузке на систему.
Еще один распространенный и очень надежный паттерн — создание отдельного объекта для хранения последнего использованного номера. Обычно для этого используют непериодический регистр сведений.
Разберем по шагам:
"ПоследниеНомераОбъектов"."ИмяСправочника", "Организация")."ПоследнийНомер" (число).Алгоритм работы с таким регистром внутри транзакции должен быть следующим:
Пример кода:
Процедура ПрисвоитьСледующийНомер(НовыйОбъект)
НачатьТранзакцию();
// 1. Блокируем запись в регистре-нумераторе
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрСведений.ПоследниеНомераОбъектов");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
// Устанавливаем отбор по измерениям, если нумерация ведется в разрезе чего-либо
ЭлементБлокировки.УстановитьЗначение("ИмяСправочника", "МойСправочник");
Блокировка.Заблокировать();
// 2. Читаем последнее значение
МенеджерЗаписи = РегистрыСведений.ПоследниеНомераОбъектов.СоздатьМенеджерЗаписи();
МенеджерЗаписи.ИмяСправочника = "МойСправочник";
МенеджерЗаписи.Прочитать();
ПоследнийНомер = МенеджерЗаписи.ПоследнийНомер;
// 3. Увеличиваем и присваиваем
НовыйНомер = ПоследнийНомер + 1;
НовыйОбъект.Номер = НовыйНомер;
// 4. Записываем новое значение в регистр
МенеджерЗаписи.ПоследнийНомер = НовыйНомер;
МенеджерЗаписи.Записать();
ЗафиксироватьТранзакцию();
КонецПроцедуры
Этот метод очень гибок, так как позволяет настраивать сложные схемы нумерации с разными счетчиками для разных условий.
Если ваша проблема вызвана именно вложенной транзакцией (например, при проведении документа), то можно изменить архитектуру кода, чтобы избежать повторного запроса к базе данных.
Идея в том, чтобы заранее сгенерировать номера в родительской транзакции и передать их в дочернюю в качестве параметров.
Как это можно реализовать:
Массив или ТаблицуЗначений.Этот подход решает проблему изоляции транзакций наиболее прямолинейно, разделяя логику получения номеров и логику их использования.
Итог: Проблема с получением одинаковых номеров внутри транзакции — это не ошибка платформы, а следствие работы механизма изоляции СУБД. Для ее решения следует использовать системные инструменты, такие как управляемые блокировки, или применять надежные архитектурные паттерны, например, регистр-нумератор или явную передачу контекста между транзакциями.