Как избежать дублирования строк в табличной части при возникновении ошибки проведения документа

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

При разработке сложных механизмов логирования в 1С программисты часто сталкиваются с ситуацией, когда данные в табличных частях или движениях начинают дублироваться. Особенно остро эта проблема проявляется, когда запись документа обернута в блок Попытка...Исключение, а алгоритм регистрации изменений прописан в модуле объекта. Рассмотрим ситуацию: мы пытаемся провести документ, происходит ошибка, мы перехватываем ее в блоке Исключение и записываем документ просто в режиме записи. В результате строки в истории изменений удваиваются.

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

Разберем причину: почему возникают дубли?

Главная причина кроется в фундаментальном различии между состоянием базы данных и состоянием объекта в оперативной памяти (переменная ЭтотОбъект). Проанализируем стандартный сценарий:

  1. Вы вызываете метод Записать(РежимЗаписи.Проведение).
  2. Платформа открывает транзакцию и запускает обработчик ПередЗаписью.
  3. В этом обработчике ваш код сравнивает реквизиты и добавляет новые строки в табличную часть (ТЧ) документа. Теперь в оперативной памяти у объекта в ТЧ есть новые строки.
  4. Далее происходит попытка фиксации данных в СУБД. На этом этапе возникает ошибка (например, нехватка остатков или нарушение условий в ОбработкаПроведения).
  5. Транзакция в базе данных откатывается. Важный момент: изменения в таблицах БД отменились, но объект в оперативной памяти сервера 1С остался в том состоянии, в которое его привел ваш код. Строки в ТЧ никуда не исчезли.
  6. Управление переходит в блок Исключение, где вызывается Записать(РежимЗаписи.Запись).
  7. Снова запускается обработчик ПередЗаписью. Код снова видит изменения реквизитов (так как они еще не зафиксированы в БД), сравнивает их и добавляет те же самые строки в ТЧ второй раз.
  8. Документ успешно сохраняется с двойным набором строк.

Выяснив причину, мы можем перейти к вариантам решения задачи.

Решение 1: Использование коллекции ДополнительныеСвойства

Самый простой и эффективный способ «пометить» объект, чтобы не выполнять логику повторно в рамках одного серверного вызова — это использование свойства ДополнительныеСвойства. Эта коллекция живет вместе с объектом в памяти и не очищается при откате транзакции БД.

Посмотрим на пример реализации кода в модуле объекта:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    
    // Проверяем, не выполнялась ли уже логика регистрации в текущем сеансе работы с объектом
    Если ДополнительныеСвойства.Свойство("ИсторияУжеЗаписана") Тогда
        Возврат;
    КонецЕсли;

    // Логика сравнения реквизитов
    Если Реквизит1 <> Ссылка.Реквизит1 Тогда
        НоваяСтрока = ИсторияИзменений.Добавить();
        НоваяСтрока.ИмяРеквизита = "Реквизит1";
        НоваяСтрока.СтароеЗначение = Ссылка.Реквизит1;
        НоваяСтрока.НовоеЗначение = Реквизит1;
        НоваяСтрока.ДатаИзменения = ТекущаяДата();
    КонецЕсли;

    // Устанавливаем флаг, чтобы при повторном вызове в блоке Исключение код пропустил этот этап
    ДополнительныеСвойства.Вставить("ИсторияУжеЗаписана", Истина);

КонецПроцедуры

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

Решение 2: Платформенный механизм «История данных»

Начиная с версии платформы 8.3.11, разработчикам доступен встроенный механизм История данных. Использование табличной части для хранения истории — это, по сути, «изобретение велосипеда», которое несет в себе риски производительности. Рассмотрим преимущества стандартного механизма:

Чтобы включить его, достаточно в свойствах объекта метаданных (документа) на вкладке «Основные» нажать кнопку «История данных» и выбрать нужные реквизиты. Это решение избавит вас от необходимости писать и поддерживать сложный код в ПередЗаписью.

Решение 3: Перенос логики в ПриЗаписи или использование независимых регистров

Проанализируем ситуацию: если мы храним историю изменений в самом документе, мы неизбежно увеличиваем его размер. Более архитектурно верным решением будет использование Независимого регистра сведений.

Если записывать историю в регистр в процедуре ПриЗаписи модуля объекта, то:

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

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

Решение 4: Принудительное перечитывание объекта

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


Попытка
    ДокументОбъект.Записать(РежимЗаписи.Проведение);
Исключение
    // Ошибка произошла, объект в памяти "грязный" (содержит добавленные строки ТЧ)
    // Перечитываем состояние объекта из базы данных, чтобы сбросить изменения в памяти
    ДокументОбъект.Прочитать(); 
    
    // Теперь ТЧ снова пуста (или в том состоянии, в котором она в БД), 
    // и мы можем пробовать записать объект снова
    ДокументОбъект.Записать(РежимЗаписи.Запись);
КонецПопытки

Важно: Метод Прочитать() сбросит все изменения реквизитов, которые были внесены в объект перед первой попыткой записи, если они не были сохранены. Этот метод стоит использовать с осторожностью.

Почему хранение истории в Табличной Части — плохая практика?

В ходе обсуждения на форуме эксперты справедливо отметили, что ТЧ — неудачное место для логов. Проанализируем основные риски:

Рекомендация: Если заказчик настаивает на хранении истории «внутри документа», постарайтесь убедить его использовать отдельную кнопку «Посмотреть историю», которая будет открывать данные из регистра сведений или интерфейса «Истории данных». Это сохранит производительность системы и чистоту кода.

Итоги

Для решения проблемы дублирования строк при ошибке проведения мы рекомендуем использовать флаги в ДополнительныхСвойствах как самый быстрый и стабильный программный метод. Однако в долгосрочной перспективе лучшим выбором станет переход на платформенный механизм Истории данных или использование Регистров сведений.

← На главную