Как выгрузить документ в файл при его записи в 1С 7.7 и избежать блокировок базы данных?

Программист 1С v7.7 IT и автоматизация бизнеса
← На главную

Разработчики, работающие с платформой 1С:Предприятие 7.7, часто сталкиваются с архитектурными ограничениями этой версии при попытке реализовать логику, привычную для 1С 8.x. Одной из таких проблем является отсутствие предопределенной процедуры ПриЗаписи() в модуле документа (аналоге модуля объекта в v8). Данная процедура существует только в модуле формы документа, что делает невозможным штатный перехват события записи, если документ создается или изменяется программно.

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

Почему выгрузка файла внутри транзакции записи — это плохая идея?

Выясним причину, почему непосредственное создание и запись физического файла на диск в момент записи или проведения документа в 1С 7.7 крайне не рекомендуется. При записи документа платформа открывает транзакцию базы данных и накладывает монопольные блокировки на системные таблицы. Если внутри этого процесса мы начинаем выполнять дисковые операции ввода-вывода (создание текстового файла, XML или JSON), время выполнения транзакции возрастает в сотни раз. Другие сеансы, пытающиеся записать свои документы, выстраиваются в очередь и в итоге «отваливаются» по таймауту блокировки.

Для решения этой проблемы мы должны разделить процесс на два этапа:

  1. Быстрая фиксация факта изменения документа в базе данных (минимальная транзакция).
  2. Асинхронное считывание измененных данных и их последующая выгрузка в файлы внешними службами или фоновыми процессами.

Рассмотрим подробнее доступные технические решения.

Решение 1. Использование предопределенных событий формы документа

Если все изменения вносятся пользователями интерактивно через интерфейс, мы можем использовать процедуру ПриЗаписи() в модуле формы документа. Однако, чтобы не заставлять пользователя ждать окончания формирования файла, мы можем применить внешнюю компоненту FormEx.

С помощью FormEx мы можем перехватить событие закрытия формы и выполнить постобработку. Проанализируем пример использования процедуры ПослеЗакрытия():


// Процедура в модуле формы документа
Процедура ПриЗаписи()
    // Устанавливаем признак того, что документ был изменен интерактивно
    глДокументИзменен = 1;
КонецПроцедуры

// Использование события FormEx после уничтожения окна формы
Процедура ПослеЗакрытия(Контекст)
    Если глДокументИзменен = 1 Тогда
        // Вызываем глобальную процедуру для регистрации изменений
        глЗаписатьСтрокуЛога(Контекст.ТекущийДокумент());
        глДокументИзменен = 0;
    КонецЕсли;
КонецПроцедуры

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

Решение 2. Использование служебного реквизита-флага (Асинхронная выгрузка)

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

Шаг 1. Создание реквизита. Добавим в метаданные документа служебный реквизит, например, ФлагВыгрузки (тип: Число, длина 1). По умолчанию при создании документа он будет равен 0.

Шаг 2. Маркировка документа. В форме документа при интерактивной записи или в коде программного создания взводим этот флаг в значение 1. При этом сама выгрузка в файл не производится:


// В коде программного создания документа
Док = СоздатьОбъект("Документ.РасходнаяНакладная");
Док.Новый();
// ... Заполнение реквизитов ...
Док.ФлагВыгрузки = 1; // Помечаем для выгрузки
Док.Записать();

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


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

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

Решение 3. Перехват записи на уровне внешней компоненты «1С++»

Для разработчиков, готовых использовать внешние библиотеки, идеальным решением станет использование класса Перехватчик из библиотеки 1С++ (1cpp.dll). Этот механизм позволяет перехватывать вызовы любых методов платформы 1С 7.7, включая запись объектов, даже если она инициирована программно.

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


// В глобальном модуле при начале работы системы инициализируем перехватчик
Процедура ПриНачалеРаботыСистемы()
    ЗагрузитьВнешнююКомпоненту("1cpp.dll");
    Перехватчик = СоздатьОбъект("Перехватчик");
    Перехватчик.ПерехватитьМетод("Документ", "Записать", "глПриЗаписиДокумента");
КонецПроцедуры

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

Использование 1С++ избавляет от необходимости модифицировать структуру метаданных (добавлять новые реквизиты) и переписывать код сотен форм существующих документов.

Решение 4. Использование SQLite в качестве неблокирующего буфера

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

Для создания абсолютно независимой очереди изменений мы можем применить встраиваемую СУБД SQLite через компоненту 1sqlite.dll. Схема работы выглядит следующим образом:

  1. При записи документа в 1С 7.7 (в форме или через перехватчик) мы мгновенно отправляем короткий SQL-запрос во внешнюю базу данных SQLite, которая может располагаться в оперативной памяти (память типа :memory:) или на быстром локальном диске (SSD).
  2. Запрос содержит только внутренний идентификатор документа (IDD) и тип операции. Запись в SQLite происходит за доли миллисекунды и абсолютно не блокирует основную базу данных 1С (как DBF, так и MS SQL).
  3. Внешний скрипт (например, написанный на Python, C# или отдельная сессия 1С) непрерывно опрашивает базу SQLite, извлекает ID измененных документов, считывает их данные напрямую из основной БД и формирует файлы выгрузки.

Посмотрим на пример записи события во внешний буфер SQLite на языке 1С 7.7:


Процедура глЗарегистрироватьВSQLite(КонтДок)
    БазаSQLite = СоздатьОбъект("SQLiteBase");
    БазаSQLite.Открыть("C:\Queue\changes_queue.db");
    
    ID_Док = ЗначениеВСтрокеВнутр(КонтДок.ТекущийДокумент());
    ВремяИзменения = ТекущееВремя();
    
    // Формируем запрос на добавление записи в очередь изменений
    ТекстЗапроса = "INSERT INTO Changes (DocID, ChangeTime, Processed) VALUES ('" + ID_Док + "', '" + ВремяИзменения + "', 0);";
    БазаSQLite.Выполнить(ТекстЗапроса);
    БазаSQLite.Закрыть();
КонецПроцедуры

Подведем итоги

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

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

← На главную