Как быстро получить движения документа через COM-соединение в 1С?

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

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

В чем кроется причина медленной работы COM-соединений?

Прежде чем перейти к написанию оптимизированного кода, давайте разберем физику процесса. Основным "бутылочным горлышком" (bottleneck) при работе через COM-соединение является так называемый маршаллинг (marshalling) — процесс преобразования и передачи данных между различными адресными пространствами (или процессами операционной системы). Каждое обращение к свойству или методу COM-объекта инициирует сетевой или межпроцессный вызов (round-trip). Если вы получаете коллекцию строк из внешней базы и обходите ее в цикле на стороне вызывающей системы, обращаясь к свойствам каждой строки, программа выполняет сотни и тысячи мелких запросов, что фатально снижает скорость работы.

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


Анализ неоптимального решения: Запрос к виртуальной таблице оборотов

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


ЗапросТЧ = Соединение.NewObject("Запрос");
ЗапросТЧ.Текст = 
    "ВЫБРАТЬ
    |   *
    |ИЗ
    |   РегистрБухгалтерии.Хозрасчетный.Обороты(&ДатаНач, &ДатаКон, Регистратор, , , , , ) КАК ХозрасчетныйОбороты
    |ГДЕ
    |   ХозрасчетныйОбороты.Регистратор = &Ссылка";

ЗапросТЧ.УстановитьПараметр("Ссылка", Выборка.Ссылка);
ЗапросТЧ.УстановитьПараметр("ДатаНач", ДатаНач);
ЗапросТЧ.УстановитьПараметр("ДатаКон", КонецДня(ДатаКон));

Выясним причину, почему данный запрос работает крайне медленно:

  1. Использование виртуальной таблицы оборотов: Виртуальные таблицы (особенно остатков и оборотов) предназначены для получения агрегированных данных за период. СУБД тратит значительные ресурсы на расчет итогов, а последующая фильтрация по полю Регистратор в секции ГДЕ сводит на нет все механизмы оптимизации СУБД.
  2. Выборка всех полей через звездочку (*): Использование символа * в запросах к регистрам бухгалтерии заставляет систему выбирать избыточное количество системных и субконто-полей, что сильно перегружает сетевой трафик при передаче данных через COM.

Решение №1: Использование физической таблицы регистра вместо виртуальной

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

Рассмотрим пример правильного построения запроса:


ЗапросТЧ = Соединение.NewObject("Запрос");
ЗапросТЧ.Текст = 
    "ВЫБРАТЬ
    |   Хозрасчетный.Период КАК Период,
    |   Хозрасчетный.СчетДт КАК СчетДт,
    |   Хозрасчетный.СчетКт КАК СчетКт,
    |   Хозрасчетный.Сумма КАК Сумма,
    |   Хозрасчетный.Содержание КАК Содержание
    |ИЗ
    |   РегистрБухгалтерии.Хозрасчетный КАК Хозрасчетный
    |ГДЕ
    |   Хозрасчетный.Регистратор = &Регистратор";

ЗапросТЧ.УстановитьПараметр("Регистратор", СсылкаНаДокумент);
РезультатЗапроса = ЗапросТЧ.Выполнить().Выгрузить();

Этот запрос отработает на порядок быстрее, поскольку СУБД выполнит прямое сканирование индекса по конкретному документу-регистратору.


Решение №2: Использование набора записей регистра (Самый быстрый способ без запросов)

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

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


// Создаем объект набора записей регистра во внешней базе через COM
НаборЗаписей = Соединение.РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();

// Устанавливаем жесткий отбор по регистратору (документу)
НаборЗаписей.Отбор.Регистратор.Установить(СсылкаНаРегистратор);

// Читаем данные напрямую из СУБД
НаборЗаписей.Прочитать();

// Выгружаем движения в локальную таблицу значений COM-соединения
ТаблицаДвижений = НаборЗаписей.Выгрузить();

Преимущества этого подхода:


Решение №3: Борьба с маршаллингом при помощи сериализации данных в JSON

Даже если вы применили чтение набора записей, последующий обход таблицы ТаблицаДвижений в вызывающей базе через конструкцию Для каждого Строка Из... будет работать медленно из-за постоянных COM-вызовов для получения значений колонок. Чтобы обойти это ограничение, применим метод сериализации данных на стороне COM-сервера.

Идея заключается в том, чтобы на стороне внешней базы упаковать всю таблицу движений в плоскую строку (например, в формате JSON), передать эту строку во внешнюю базу за один COM-вызов, а затем быстро десериализовать ее в локальную таблицу значений. Таким образом, вместо сотен вызовов для каждой строки мы сделаем ровно один вызов.

Рассмотрим, как реализовать этот алгоритм на практике:


// Код выполняется в вашей текущей базе
Функция ПолучитьДвиженияЧерезJSON(Соединение, СсылкаНаРегистратор)
    
    // Формируем текст скрипта для выполнения на стороне COM-сервера
    // Если в вашей конфигурации есть общий модуль для работы с JSON, используйте его.
    // В данном примере мы передаем управление на сторону COM-сервера для сериализации.
    
    КодДляВыполнения = 
        "Набор = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
        |Набор.Отбор.Регистратор.Установить(ПараметрСсылка);
        |Набор.Прочитать();
        |ТЗ = Набор.Выгрузить();
        |
        |// Преобразуем ТЗ в массив структур для сериализации в JSON
        |МассивСтрок = Новый Массив;
        |Для Каждого СтрокаТЗ Из ТЗ Цикл
        |   СтруктураСтроки = Новый Структура;
        |   СтруктураСтроки.Вставить(""Период"", СтрокаТЗ.Период);
        |   СтруктураСтроки.Вставить(""СчетДт"", Строка(СтрокаТЗ.СчетДт.Код)); // Передаем простые типы
        |   СтруктураСтроки.Вставить(""СчетКт"", Строка(СтрокаТЗ.СчетКт.Код));
        |   СтруктураСтроки.Вставить(""Сумма"", СтрокаТЗ.Сумма);
        |   СтруктураСтроки.Вставить(""Содержание"", СтрокаТЗ.Содержание);
        |   МассивСтрок.Добавить(СтруктураСтроки);
        |КонецЦикла;
        |
        |ЗаписьJSON = Новый ЗаписьJSON;
        |ЗаписьJSON.УстановитьСтроку();
        |ЗаписатьJSON(ЗаписьJSON, МассивСтрок);
        |РезультатСтрокой = ЗаписьJSON.Закрыть();";
        
    // Используем встроенный механизм выполнения кода во внешней базе (если доступно)
    // Либо реализуем аналогичную функцию в модуле внешнего соединения базы-источника.
    // В простейшем случае мы можем выполнить этот код через встроенный запуск:
    
    // Внимание: Передача сложных типов (ссылок) через границу COM нежелательна,
    // поэтому в структуре мы преобразуем счета в их текстовые коды (строки).
    
    // Предположим, у нас во внешней базе есть экспортная функция в общем модуле:
    // Функция СериализоватьДвижения(СсылкаНаДокумент) Экспорт
    
    СтрокаJSON = Соединение.ОбщийМодульОбмена.СериализоватьДвижения(СсылкаНаРегистратор);
    
    // Теперь десериализуем строку JSON локально в нашей базе
    ЧтениеJSON = Новый ЧтениеJSON;
    ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
    
    МассивДвижений = ПрочитатьJSON(ЧтениеJSON);
    ЧтениеJSON.Закрыть();
    
    Возврат МассивДвижений; // Получаем готовый массив локальных структур без COM-зависимостей
КонецФункции

Применение сериализации данных в JSON сокращает время обмена данными по COM-каналам в 5–10 раз, превращая сложный распределенный процесс в моментальную передачу одной текстовой строки.


Сводная таблица рекомендаций по работе с COM

Для наглядности проанализируем основные подходы и выберем оптимальный вариант для вашей архитектуры:

Метод получения движений Скорость работы Нагрузка на СУБД Рекомендация к использованию
Запрос к виртуальной таблице .Обороты с отбором ГДЕ Регистратор = ... Очень медленно Крайне высокая Не рекомендуется (антипаттерн)
Получение объекта документа ПолучитьОбъект().Движения Медленно Средняя Допускается только при единичных вызовах
Запрос к физической таблице движений регистра Быстро Низкая Рекомендуется для сложных выборок с фильтрами
Чтение набора записей СоздатьНаборЗаписей() Очень быстро Минимальная Рекомендуется как стандартное решение
Чтение набора записей + Сериализация в JSON Максимально быстро Минимальная Лучший выбор для больших объемов данных

Применяя данные подходы и отдавая предпочтение объекту СоздатьНаборЗаписей() в сочетании с сериализацией сложных типов в простые строки JSON, вы сможете добиться мгновенного отклика интеграционных механизмов даже при работе со старыми версиями платформы через COM-соединение.

← На главную