Разработчики, работающие с платформой 1С:Предприятие 7.7, часто сталкиваются с архитектурными ограничениями этой версии при попытке реализовать логику, привычную для 1С 8.x. Одной из таких проблем является отсутствие предопределенной процедуры ПриЗаписи() в модуле документа (аналоге модуля объекта в v8). Данная процедура существует только в модуле формы документа, что делает невозможным штатный перехват события записи, если документ создается или изменяется программно.
Ситуация усложняется, если в базе данных фиксируется высокая интенсивность создания документов. Попытки выполнять выгрузку непосредственно в момент записи файла приводят к длительным транзакциям, избыточным блокировкам таблиц (таких как _1SJOURN) и взаимным тупикам (deadlocks). В этой статье мы подробно проанализируем ситуацию, разберем причины возникновения блокировок и рассмотрим несколько эффективных способов реализации асинхронной выгрузки документов в файл.
Выясним причину, почему непосредственное создание и запись физического файла на диск в момент записи или проведения документа в 1С 7.7 крайне не рекомендуется. При записи документа платформа открывает транзакцию базы данных и накладывает монопольные блокировки на системные таблицы. Если внутри этого процесса мы начинаем выполнять дисковые операции ввода-вывода (создание текстового файла, XML или JSON), время выполнения транзакции возрастает в сотни раз. Другие сеансы, пытающиеся записать свои документы, выстраиваются в очередь и в итоге «отваливаются» по таймауту блокировки.
Для решения этой проблемы мы должны разделить процесс на два этапа:
Рассмотрим подробнее доступные технические решения.
Если все изменения вносятся пользователями интерактивно через интерфейс, мы можем использовать процедуру ПриЗаписи() в модуле формы документа. Однако, чтобы не заставлять пользователя ждать окончания формирования файла, мы можем применить внешнюю компоненту FormEx.
С помощью FormEx мы можем перехватить событие закрытия формы и выполнить постобработку. Проанализируем пример использования процедуры ПослеЗакрытия():
// Процедура в модуле формы документа
Процедура ПриЗаписи()
// Устанавливаем признак того, что документ был изменен интерактивно
глДокументИзменен = 1;
КонецПроцедуры
// Использование события FormEx после уничтожения окна формы
Процедура ПослеЗакрытия(Контекст)
Если глДокументИзменен = 1 Тогда
// Вызываем глобальную процедуру для регистрации изменений
глЗаписатьСтрокуЛога(Контекст.ТекущийДокумент());
глДокументИзменен = 0;
КонецЕсли;
КонецПроцедуры
Ограничение метода: Данный способ абсолютно не подходит, если документы создаются или изменяются программно (например, при загрузке из других систем, групповых обработках или проведении документов роботом), так как форма документа в этих сценариях не открывается.
Разберем по шагам наиболее стабильный и отказоустойчивый метод без использования сторонних компонент. Он заключается в маркировке измененных документов и их последующей обработке роботом.
Шаг 1. Создание реквизита. Добавим в метаданные документа служебный реквизит, например, ФлагВыгрузки (тип: Число, длина 1). По умолчанию при создании документа он будет равен 0.
Шаг 2. Маркировка документа. В форме документа при интерактивной записи или в коде программного создания взводим этот флаг в значение 1. При этом сама выгрузка в файл не производится:
// В коде программного создания документа
Док = СоздатьОбъект("Документ.РасходнаяНакладная");
Док.Новый();
// ... Заполнение реквизитов ...
Док.ФлагВыгрузки = 1; // Помечаем для выгрузки
Док.Записать();
Шаг 3. Создание робота-обработчика. Настроим отдельный сеанс 1С («робот»), который будет запущен в фоновом режиме (или будем использовать встроенную процедуру ОбработкаОжидания() в глобальном модуле одного из административных сеансов). Робот с определенной периодичностью выполняет поиск измененных документов, выгружает их и сбрасывает флаг:
Процедура ВыполнитьВыгрузкуПомеченных()
Док = СоздатьОбъект("Документ.РасходнаяНакладная");
Док.ВыбратьДокументы();
// Используем прямой запрос или перебор по реквизиту (если он индексирован)
// Для оптимизации рекомендуется сделать реквизит ФлагВыгрузки сортируемым (индексированным)
Если Док.ВыбратьПоРеквизиту("ФлагВыгрузки", 1, 0) = 1 Тогда
Пока Док.ПолучитьДокумент() = 1 Цикл
Попытка
// Вызываем процедуру непосредственной выгрузки в файл
ВыгрузитьДокументВФайл(Док.ТекущийДокумент());
// Снимаем флаг, чтобы не выгружать повторно
Док.ФлагВыгрузки = 0;
Док.Записать();
Исключение
Сообщить("Ошибка выгрузки документа " + Док.ТекущийДокумент() + ": " + ОписаниеОшибки());
КонецПопытки;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Этот подход гарантирует, что запись файлов происходит вне основных транзакций пользователей, а значит, полностью исключает влияние дисковой системы на скорость оперативной работы базы данных.
Для разработчиков, готовых использовать внешние библиотеки, идеальным решением станет использование класса Перехватчик из библиотеки 1С++ (1cpp.dll). Этот механизм позволяет перехватывать вызовы любых методов платформы 1С 7.7, включая запись объектов, даже если она инициирована программно.
С помощью класса Перехватчик мы можем переопределить системное событие записи. Посмотрим на пример концептуальной реализации:
// В глобальном модуле при начале работы системы инициализируем перехватчик
Процедура ПриНачалеРаботыСистемы()
ЗагрузитьВнешнююКомпоненту("1cpp.dll");
Перехватчик = СоздатьОбъект("Перехватчик");
Перехватчик.ПерехватитьМетод("Документ", "Записать", "глПриЗаписиДокумента");
КонецПроцедуры
// Глобальная функция, которая будет вызываться вместо или вместе со стандартным методом Записать()
Функция глПриЗаписиДокумента(КонтекстДокумента, РежимЗаписи, РежимПроведения) Экспорт
// Здесь мы получаем доступ к объекту документа до его физической записи в БД
// Регистрируем факт изменения документа во внешней базе данных или очереди
РегистрироватьИзменение(КонтекстДокумента.ТекущийДокумент());
// Возвращаем управление штатному методу записи
Возврат ВыполнитьОригинальныйМетод(КонтекстДокумента, РежимЗаписи, РежимПроведения);
КонецФункции
Использование 1С++ избавляет от необходимости модифицировать структуру метаданных (добавлять новые реквизиты) и переписывать код сотен форм существующих документов.
Если интенсивность транзакций крайне высока, запись факта изменения документа обратно в ту же базу данных 1С (даже простое снятие флага или запись в служебный справочник) сама по себе может приводить к взаимным блокировкам.
Для создания абсолютно независимой очереди изменений мы можем применить встраиваемую СУБД SQLite через компоненту 1sqlite.dll. Схема работы выглядит следующим образом:
:memory:) или на быстром локальном диске (SSD).Посмотрим на пример записи события во внешний буфер 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).