Как получить регистры, по которым есть движения у документа в 1С

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

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

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

Выясняем причину: почему количество движений всегда равно нулю?

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


Для Каждого Рег Из Док.Движения Цикл
    Если Рег.Количество() <> 0 Тогда
        МассивРегистров.Добавить(Строка(Рег));
    КонецЕсли;
КонецЦикла;

При запуске этого кода выясняется, что условие Рег.Количество() <> 0 никогда не выполняется. Почему платформа ведет себя подобным образом?

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

Платформа **не считывает** эти данные из базы данных автоматически при обращении к объекту документа, чтобы не тратить ресурсы сервера и сетевой трафик. Физически в оперативной памяти эти наборы записей остаются пустыми до тех пор, пока мы явно не прикажем системе прочитать их из СУБД.

Решение 1: Объектный метод с использованием «Прочитать()»

Самый простой и очевидный способ заставить платформу загрузить движения в память — использовать метод Прочитать() для каждого набора записей. Давайте посмотрим на пример такого кода:


ДокОбъект = СсылкаНаДокумент.ПолучитьОбъект();

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

Этот код абсолютно рабочий. Метод Прочитать() отправляет запрос к СУБД, заполняет коллекцию записей конкретного регистра по данному регистратору, после чего метод Количество() возвращает корректное значение.

Однако у этого метода есть **критические недостатки**, из-за которых его категорически не рекомендуется использовать в реальных рабочих базах данных (особенно в циклах при обработке большого количества документов).

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

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

  1. Использование метода ПолучитьОбъект(): Метод ПолучитьОбъект() — одна из самых «тяжелых» операций в 1С. Чтобы получить объект документа, платформа считывает из базы данных абсолютно все его реквизиты, а также полностью загружает все табличные части в оперативную память. Если у вас документ с тысячами строк в табличной части (например, «Реализация товаров и услуг» или «Начисление зарплаты»), вы получите огромную и абсолютно бесполезную нагрузку на оперативную память сервера приложений.
  2. Проблема N+1 запросов при чтении движений: Допустим, у нашего документа в метаданных разрешены движения по 15 регистрам (что является стандартной ситуацией для ERP или УПП). При вызове НаборЗаписей.Прочитать() в цикле платформа выполнит ровно 15 отдельных SQL-запросов к СУБД — по одному запросу на каждый регистр. Если вы обрабатываете таким образом 100 документов, система совершит 1500 запросов к базе данных! Это вызовет серьезные задержки и блокировки.
  3. Избыточное потребление памяти: Метод Прочитать() полностью загружает все строки движений в память. Если у документа по какому-то регистру накопления сформировано 10 000 проводок, все они будут подняты на сервер приложений только ради того, чтобы вы смогли проверить, пустой ли набор.

Решение 2: Оптимальный способ через динамический запрос (Best Practice)

Как же решить эту задачу правильно и быстро? Профессиональная разработка на платформе 1С:Предприятие 8 подразумевает использование запросов для любых проверок существования данных. Нам не нужно получать объекты документов и не нужно читать все их движения. Для автоматической сверки результатов таких запросов или контроля проводок после завершения обмена данными применяется инструмент автоматической сверки бухгалтерских и налоговых проводок между базами.

Мы можем динамически построить один единственный запрос к базам данных СУБД, используя метаданные документа. Запрос будет проверять наличие хотя бы одной записи по каждому возможному регистру с использованием конструкции ВЫБРАТЬ ПЕРВЫЕ 1.

Рассмотрим подробнее этот алгоритм и разберем его по шагам на примере готовой функции:


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

Почему этот запрос работает быстрее всего?

Проанализируем преимущества разработанного нами запроса:

Важные нюансы и граничные случаи

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

1. Независимые регистры сведений. Описанный нами алгоритм и метаданные движений работают только с подчиненными регистрами (у которых в конфигураторе свойство «Режим записи» установлено в значение «Подчинение регистратору»). Независимые регистры сведений не привязываются к документу через стандартный реквизит Регистратор, поэтому они не будут отражены в метаданных движений документа. Если ваш документ делает записи в независимый регистр сведений программно, этот факт придется отслеживать отдельно.

2. Пустые наборы записей. Иногда при отмене проведения документа или при его перезаписи система записывает в таблицы движений пустые наборы. Физически в базе данных SQL записи очищаются. Наш оптимальный запрос отработает правильно: он не найдет строк и покажет, что движений нет. В то же время, если вы будете использовать объектный метод, Прочитать() вернет пустую коллекцию, и Количество() также покажет 0. Результаты совпадают, но запрос делает это в сотни раз быстрее.

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

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

← На главную