Как записать данные в регистр сведений в транзакции и не потерять их при ее отмене

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

В процессе разработки сложных механизмов автоматизации, таких как загрузка данных из внешних систем или комплексное формирование цепочки документов, мы часто сталкиваемся с необходимостью логирования процесса. Однако возникает фундаментальная проблема: если вся операция выполняется в рамках одной транзакции (для обеспечения целостности данных), то при возникновении любой ошибки и последующей команде ОтменитьТранзакцию(), все записи в регистры сведений, сделанные «по ходу дела», также будут стерты. Рассмотрим подробно, как обойти это ограничение платформы 1С и реализовать механизм «автономных транзакций».

Понимание проблемы атомарности транзакции

Прежде чем мы перейдем к решениям, проанализируем ситуацию. Транзакция в 1С подчиняется принципу атомарности: либо все действия внутри нее фиксируются в базе данных, либо не фиксируется ничего. Это критически важно для бухгалтерского или складского учета, но крайне неудобно для системного логирования. Если мы создаем пять документов и на пятом происходит ошибка, мы хотим знать, что именно пошло не так с первыми четырьмя, сохранив эти записи в специальном регистре сведений.

Стандартными средствами внутри одного потока выполнения (одного сеанса) записать данные «частично» невозможно. Нам необходимо либо вывести процесс логирования за пределы текущей транзакции, либо сохранить информацию в оперативной памяти до момента завершения или отката транзакции.

Решение 1: Накопление данных в памяти и запись после отката

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

Разберем по шагам реализацию этого метода:

  1. Создаем переменную-контейнер перед началом транзакции.
  2. В процессе работы наполняем этот контейнер структурами с данными для лога.
  3. Оборачиваем основной код в блок Попытка — Исключение.
  4. В блоке Исключение сначала вызываем ОтменитьТранзакцию(), а затем запускаем процедуру записи накопленных данных из памяти в базу.

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


ПеременнаяЛог = Новый Массив; // Данные в памяти не удаляются при откате транзакции

НачатьТранзакцию();
Попытка
    // Имитация процесса создания документов
    Для Каждого Заказ Из СписокЗаказов Цикл
        // Добавляем запись в лог (в память)
        НоваяЗаписьЛога = Новый Структура("Дата, Сообщение", ТекущаяДата(), "Обработка заказа: " + Заказ.Номер);
        ПеременнаяЛог.Добавить(НоваяЗаписьЛога);
        
        // Тут может произойти ошибка
        ЗаказОбъект = Заказ.ПолучитьОбъект();
        ЗаказОбъект.Записать(РежимЗаписиДокумента.Проведение);
    КонецЦикла;
    
    ЗафиксироватьТранзакцию();
Исключение
    Если ТранзакцияАктивна() Тогда
        ОтменитьТранзакцию();
    КонецЕсли;
    
    // ТРАНЗАКЦИЯ ОТМЕНЕНА, но ПеременнаяЛог все еще содержит данные!
    // Теперь мы можем спокойно записать их в регистр сведений
    ЗаписатьНакопленныйЛогВБазу(ПеременнаяЛог);
    
    ВызватьИсключение КраткоеОписаниеОшибки(ИнформацияОбОшибке());
КонецПопытки;

Решение 2: Использование фоновых заданий

Если нам критически важно записывать логи в режиме «реального времени» (например, чтобы администратор мог видеть прогресс выполнения длительной операции), метод с накоплением в памяти не подойдет. В этом случае мы можем использовать механизм фоновых заданий. Каждое фоновое задание запускается в отдельном сеансе и имеет свой собственный контекст транзакций.

Разберем, как это работает: внутри основной транзакции мы вызываем процедуру, которая запускает фоновое задание для записи одной строки лога. Поскольку фоновое задание выполняется независимо, откат основной транзакции никак не повлияет на данные, уже записанные фоновым заданием.

Пример вызова фонового задания для логирования:


Процедура ЗаписатьЛогАвтономно(ТекстСообщения)
    ПараметрыПроцедуры = Новый Массив;
    ПараметрыПроцедуры.Добавить(ТекстСообщения);
    
    // Запускаем фоновое задание. Оно создаст новый сеанс и свою транзакцию.
    ФоновыеЗадания.Выполнить("МойОбщийМодуль.ЗаписатьВРегистрЛога", ПараметрыПроцедуры);
КонецПроцедуры

Важный нюанс: Частое создание фоновых заданий (например, для каждой строки лога в цикле из 10 000 итераций) создаст колоссальную нагрузку на сервер приложений 1С и может привести к исчерпанию лицензий или оперативной памяти. Этот метод хорош только для редких, ключевых записей.

Решение 3: HTTP-сервис как «внешний» регистратор

Проанализируем еще один современный подход — использование внутреннего HTTP-сервиса. Мы можем опубликовать в нашей же базе простой HTTP-сервис, который принимает данные и записывает их в регистр логов. Когда основной код в транзакции делает HTTP-запрос к самому себе (к localhost), платформа создает новое соединение. Это соединение не видит транзакцию вызывающего потока.

Плюсы этого метода:

Решение 4: Журнал регистрации 1С

Выясним причину, по которой многие разработчики забывают про стандартный ЖурналРегистрации. Данные в него пишутся напрямую в файлы (или базу SQLite/SQL в зависимости от версии платформы), минуя механизм транзакций основной базы данных. Если ваша цель — просто сохранить историю событий для последующего анализа администратором, использование ЗаписатьВЖурналРегистрации является самым быстрым и «дешевым» с точки зрения ресурсов способом.

Пример использования:


ЗаписатьВЖурналРегистрации("ЗагрузкаДанных", 
    УровеньЖурналаРегистрации.Информация, 
    , 
    , 
    "Документ успешно обработан: " + НомерДокумента);

Чтобы пользователи могли анализировать эти данные в удобном виде (с отборами и отчетами), можно написать простую обработку, которая считывает данные из журнала регистрации с помощью метода ВыгрузитьЖурналРегистрации() и выводит их в таблицу на форме.

Решение 5: Использование параметров сеанса

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

Мы можем создать параметр сеанса с типом ХранилищеЗначения. В него мы будем помещать наш массив структур лога. Поскольку параметры сеанса живут в памяти серверного вызова, они сохраняют свое состояние даже после отката транзакции СУБД. После того как управление вернется в верхнюю точку и транзакция будет отменена в блоке Исключение, мы извлечем данные из параметра сеанса и запишем их в регистр.

Алгоритм работы:

  1. В начале процесса инициализируем параметр сеанса пустым массивом.
  2. В любой вложенной функции получаем массив из параметра сеанса, добавляем запись и помещаем обратно в ХранилищеЗначения.
  3. При ошибке в главном блоке читаем финальное состояние массива из параметра сеанса и фиксируем в базу.

Выводы и рекомендации

Проанализировав все варианты, можно сделать следующие выводы:

Помните, что транзакция — это мощный инструмент обеспечения целостности, и попытки «обмануть» ее всегда требуют четкого понимания того, как платформа управляет сеансами и памятью. Выбирайте метод исходя из ваших требований к производительности и оперативности доступа к данным лога.

← На главную