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

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

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

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

Анализ исходной проблемы: почему транзакция не фиксировалась?

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


Процедура ВыбратьВсеДокументы()
    МассивДокументов = Метаданные.Документы;
    ВсегоДок = 0;  
    Для каждого СтрМас Из МассивДокументов Цикл
        // НачатьТранзакцию(); // Код был закомментирован автором для проверки
        КолДок = 0;
        Если СтрМас.Реквизиты.Найти("Организация") = Неопределено Тогда
            Продолжить; // Критическая точка!
        КонецЕсли;
        
        ДокТип = СтрМас.Имя;
        Запрос = Новый Запрос;
        Запрос.Текст =
        "ВЫБРАТЬ
        |    " + ДокТип + ".Ссылка КАК Ссылка
        |ИЗ
        |    Документ." + ДокТип + " КАК " + ДокТип + " Где " + ДокТип + ".Организация <> &Организация";
        Запрос.УстановитьПараметр("Организация", Организация);
        Результат = Запрос.Выполнить();
        ВыборкаДетальныеЗаписи = Результат.Выбрать();
        Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
            ВсегоДок = ВсегоДок + 1;    
            Если КолДок >= 100 Тогда
                // ЗафиксироватьТранзакцию();
                КолДок = 0;
                // НачатьТранзакцию(); 
            КонецЕсли;     
            Состояние(ВыборкаДетальныеЗаписи.Ссылка);
            Объект = ВыборкаДетальныеЗаписи.Ссылка.ПолучитьОбъект();
            Объект.Удалить();
            КолДок = КолДок + 1;
        КонецЦикла;
        // ЗафиксироватьТранзакцию();
    КонецЦикла;
КонецПроцедуры

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

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


Если СтрМас.Реквизиты.Найти("Организация") = Неопределено Тогда
    Продолжить;
КонецЕсли;

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

В платформе 1С транзакции имеют свойство вложенности. Реальная транзакция на уровне СУБД фиксируется только тогда, когда счетчик транзакций на уровне платформы возвращается к нулю. Из-за оператора Продолжить баланс между открытием и закрытием транзакций нарушился. Накопились незакрытые транзакции, и при завершении работы процедуры вся цепочка изменений была попросту отменена (произошел Rollback).

Решение проблемы рассинхронизации транзакций

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


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

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

Критическая ошибка «В данной транзакции уже происходили ошибки!»

Если в процессе обработки пакета из 100 документов при удалении хотя бы одного из них возникнет любая ошибка (например, документ заблокирован другим пользователем или сработал отказ в обработчике события ПередУдалением), транзакция на уровне СУБД помечается как «неисправная» (rollback-only).

Многие разработчики ошибочно полагают, что достаточно обернуть вызов метода Удалить() в конструкцию Попытка...Исключение, чтобы продолжить обработку. Но это не так. При попытке зафиксировать текущий пакет документов или при открытии следующей транзакции платформа выдаст неустранимое исключение: «В данной транзакции уже происходили ошибки!».

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

Опасность непосредственного удаления объектов (метод Удалить)

Метод Объект.Удалить() выполняет физическое удаление записи из таблиц СУБД в обход контроля ссылочной целостности. Рассмотрим последствия такого действия:

  1. В базе данных гарантированно останутся «битые» ссылки (записи вида <Объект не найден> ...) в связанных регистрах сведений, накопления, а также в реквизитах других документов и справочников.
  2. Это приведет к некорректной работе отчетов, сбоям при проведении связанных документов и нарушению логики бизнес-процессов.

Рекомендуемый подход к безопасному удалению:

  1. Сначала необходимо снять документ с проведения: Объект.Записать(РежимЗаписиДокумента.ОтменаПроведения). Это очистит все движения документа по регистрам.
  2. Затем следует установить пометку удаления: Объект.УстановитьПометкуУдаления(Истина).
  3. Окончательное физическое удаление нужно доверять только стандартным механизмам конфигурации (например, типовой обработке «Удаление помеченных объектов»), которая осуществляет тщательную проверку наличия ссылок на удаляемые элементы.

Эскалация блокировок СУБД и нагрузка на журнал транзакций

Массовое удаление объектов пакетами по 100 штук в транзакции создает колоссальную нагрузку на СУБД. Проанализируем два основных фактора риска:

1. Блокировки таблиц (Lock Escalation):
Если конфигурация работает в режиме автоматических блокировок, СУБД при удалении множества строк может принять решение заблокировать не отдельные записи, а всю таблицу целиком. Это парализует работу пользователей: они не смогут записывать новые документы данного типа или проводить их. Чтобы избежать этого в многопользовательской среде, подобные обработки запускают либо в монопольном режиме, либо используют механизмы управляемых блокировок, явным образом блокируя только конкретные удаляемые ссылки.

2. Рост файла журнала транзакций (LDF):
Физическое удаление большого объема данных сильно нагружает дисковую подсистему. СУБД фиксирует каждое удаление в журнале транзакций. Если размер пакета слишком велик, файл LDF может стремительно вырасти и занять все свободное место на диске сервера, что приведет к аварийной остановке всей СУБД.

Правильный шаблон безопасной пакетной обработки данных

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

Рассмотрим этот алгоритм по шагам:


Процедура ВыполнитьБезопасноеПакетноеУдаление(МассивДокументовДляУдаления)
    
    РазмерПакета = 100;
    ВсегоЭлементов = МассивДокументовДляУдаления.Количество();
    Индекс = 0;
    
    Пока Индекс < ВсегоЭлементов Цикл
        
        НачатьТранзакцию();
        УспешноОбработано = Истина;
        ОшибкиПакета = Новый Массив;
        
        Для К = 1 По РазмерПакета Цикл
            Если Индекс >= ВсегоЭлементов Тогда
                Прервать;
            КонецЕсли;
            
            ТекущаяСсылка = МассивДокументовДляУдаления[Индекс];
            Индекс = Индекс + 1;
            
            Попытка
                ДокументОбъект = ТекущаяСсылка.ПолучитьОбъект();
                Если ДокументОбъект <> Неопределено Тогда
                    
                    // Шаг 1: Безопасное снятие с проведения
                    Если ДокументОбъект.Проведен Тогда
                        ДокументОбъект.Записать(РежимЗаписиДокумента.ОтменаПроведения);
                    КонецЕсли;
                    
                    // Шаг 2: Физическое удаление (или замена на УстановитьПометкуУдаления)
                    ДокументОбъект.Удалить();
                    
                КонецЕсли;
            Исключение
                // Фиксируем факт ошибки в пакете
                УспешноОбработано =  Ложь;
                ОписаниеОшибки = ПодробноеПредставлениеОшибки(ИнформацияОбОшибке());
                ОшибкиПакета.Добавить("Ошибка при удалении объекта " + ТекущаяСсылка + ": " + ОписаниеОшибки);
                
                // Прерываем цикл заполнения пакета, так как транзакция испорчена
                Прервать; 
            КонецПопытки;
        КонецЦикла;
        
        // Анализируем исход транзакции пакета
        Если УспешноОбработано Тогда
            ЗафиксироватьТранзакцию();
        Иначе
            ОтменитьТранзакцию(); // Немедленный откат всей транзакции пакета!
            
            // Записываем информацию об ошибках в Журнал Регистрации
            Для каждого Ошибка Из ОшибкиПакета Цикл
                ЗаписьЖурналаРегистрации("ПакетноеУдалениеДокументов",
                    УровеньЖурналаРегистрации.Ошибка,
                    ,
                    ,
                    Ошибка);
            EndDo;
            
            // Дополнительно: здесь можно обработать проблемные документы из этого пакета по одному 
            // вне транзакции, чтобы не останавливать удаление остальных корректных данных.
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

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

← На главную