При разработке и оптимизации конфигураций на платформе 1С:Предприятие 8 часто возникает задача: определить, в какие именно регистры (накопления, бухгалтерии, сведений или расчета) конкретный документ записал свои движения. Это необходимо для построения отчетов, проведения аудита данных, написания обработок по удалению или корректировке движений, а также при переносе данных. В ситуациях, когда требуется полная очистка базы от сведений по конкретной компании, можно воспользоваться обработкой для выборочного удаления организаций.
Начинающие разработчики часто сталкиваются с проблемой, когда при обходе коллекции движений документа свойство Количество всегда возвращает 0, хотя документ проведен и движения в интерфейсе отображаются. В этой статье мы подробно разберем, почему так происходит, рассмотрим стандартное решение этой проблемы и проанализируем, почему оно может «положить» производительность вашей базы данных. В завершение мы напишем идеальный с точки зрения быстродействия код, который рекомендуется использовать на крупных и нагруженных системах.
Рассмотрим ситуацию, с которой часто начинают разбор этой задачи. Разработчик пытается обойти коллекцию движений документа через его объектную форму и проверить количество записей в каждом регистре. Код выглядит примерно следующим образом:
Для Каждого Рег Из Док.Движения Цикл
Если Рег.Количество() <> 0 Тогда
МассивРегистров.Добавить(Строка(Рег));
КонецЕсли;
КонецЦикла;
При запуске этого кода выясняется, что условие Рег.Количество() <> 0 никогда не выполняется. Почему платформа ведет себя подобным образом?
Дело в особенностях работы объектной модели платформы 1С:Предприятие. Коллекция Движения у объекта документа (тип ДокументОбъект) инициализируется автоматически. Она содержит в себе пустые наборы записей (тип РегистрНакопленияНаборЗаписей, РегистрСведенийНаборЗаписей и т.д.) по всем регистрам, которые указаны для этого документа в качестве разрешенных движений в конфигураторе.
Платформа **не считывает** эти данные из базы данных автоматически при обращении к объекту документа, чтобы не тратить ресурсы сервера и сетевой трафик. Физически в оперативной памяти эти наборы записей остаются пустыми до тех пор, пока мы явно не прикажем системе прочитать их из СУБД.
Самый простой и очевидный способ заставить платформу загрузить движения в память — использовать метод Прочитать() для каждого набора записей. Давайте посмотрим на пример такого кода:
ДокОбъект = СсылкаНаДокумент.ПолучитьОбъект();
Для Каждого НаборЗаписей Из ДокОбъект.Движения Цикл
// Принудительно считываем данные из базы данных
НаборЗаписей.Прочитать();
// Теперь свойство Количество() вернет реальное число записей
Если НаборЗаписей.Количество() > 0 Тогда
МассивРегистров.Добавить(НаборЗаписей.Метаданные().ПолноеИмя());
Сообщить("Документ делает движения по регистру: " + НаборЗаписей.Метаданные().Синоним);
КонецЕсли;
КонецЦикла;
Этот код абсолютно рабочий. Метод Прочитать() отправляет запрос к СУБД, заполняет коллекцию записей конкретного регистра по данному регистратору, после чего метод Количество() возвращает корректное значение.
Однако у этого метода есть **критические недостатки**, из-за которых его категорически не рекомендуется использовать в реальных рабочих базах данных (особенно в циклах при обработке большого количества документов).
Давайте проанализируем, к каким последствиям приводит выполнение вышеописанного кода на серверной стороне:
ПолучитьОбъект() — одна из самых «тяжелых» операций в 1С. Чтобы получить объект документа, платформа считывает из базы данных абсолютно все его реквизиты, а также полностью загружает все табличные части в оперативную память. Если у вас документ с тысячами строк в табличной части (например, «Реализация товаров и услуг» или «Начисление зарплаты»), вы получите огромную и абсолютно бесполезную нагрузку на оперативную память сервера приложений.НаборЗаписей.Прочитать() в цикле платформа выполнит ровно 15 отдельных SQL-запросов к СУБД — по одному запросу на каждый регистр. Если вы обрабатываете таким образом 100 документов, система совершит 1500 запросов к базе данных! Это вызовет серьезные задержки и блокировки.Прочитать() полностью загружает все строки движений в память. Если у документа по какому-то регистру накопления сформировано 10 000 проводок, все они будут подняты на сервер приложений только ради того, чтобы вы смогли проверить, пустой ли набор.Как же решить эту задачу правильно и быстро? Профессиональная разработка на платформе 1С:Предприятие 8 подразумевает использование запросов для любых проверок существования данных. Нам не нужно получать объекты документов и не нужно читать все их движения. Для автоматической сверки результатов таких запросов или контроля проводок после завершения обмена данными применяется инструмент автоматической сверки бухгалтерских и налоговых проводок между базами.
Мы можем динамически построить один единственный запрос к базам данных СУБД, используя метаданные документа. Запрос будет проверять наличие хотя бы одной записи по каждому возможному регистру с использованием конструкции ВЫБРАТЬ ПЕРВЫЕ 1.
Рассмотрим подробнее этот алгоритм и разберем его по шагам на примере готовой функции:
Функция ПолучитьСписокРегистровСДвижениями(СсылкаНаДокумент) Экспорт
МассивРегистров = Новый Массив;
// Шаг 1. Получаем коллекцию метаданных всех возможных движений документа
ДвиженияМетаданные = СсылкаНаДокумент.Метаданные().Движения;
Если ДвиженияМетаданные.Количество() = 0 Тогда
Возврат МассивРегистров;
КонецЕсли;
Запрос = Новый Запрос;
ТекстЗапроса = "";
// Шаг 2. Динамически формируем текст запроса
Для Каждого МетаРегистр Из ДвиженияМетаданные Цикл
ИмяРегистра = МетаРегистр.ПолноеИмя(); // Например, "РегистрНакопления.ОстаткиТоваров"
Если ЗначениеЗаполнено(ТекстЗапроса) Тогда
ТекстЗапроса = ТекстЗапроса + " ИЛИ ";
КонецЕсли;
// Используем ВЫБРАТЬ ПЕРВЫЕ 1 для максимального ускорения работы СУБД
ТекстЗапроса = ТекстЗапроса + "ВЫБРАТЬ ПЕРВЫЕ 1 """ + ИмяРегистра + """ КАК ИмяРегистра ИЗ " + ИмяРегистра + " ГДЕ Регистратор = &Регистратор";
КонецЦикла;
// Шаг 3. Заменяем разделители на оператор объединения таблиц
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, " ИЛИ ", " ОБЪЕДИНИТЬ ВСЕ ");
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("Регистратор", СсылкаНаДокумент);
// Шаг 4. Выполняем запрос к базе данных
Попытка
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
МассивРегистров.Добавить(Выборка.ИмяРегистра);
КонецЦикла;
КонецЕсли;
Исключение
// Логируем возможные непредвиденные ошибки
ЗаписьЖурналаРегистрации("ПолучениеДвижений", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
КонецПопытки;
Возврат МассивРегистров;
КонецФункции
Проанализируем преимущества разработанного нами запроса:
ОБЪЕДИНИТЬ ВСЕ. Это экономит время на сетевой обмен между сервером приложений и сервером баз данных.ПолучитьОбъект(). Мы передаем в запрос только СсылкаНаДокумент, что экономит оперативную память сервера.При решении задач, связанных с анализом движений, необходимо учитывать несколько важных архитектурных особенностей платформы 1С:
1. Независимые регистры сведений. Описанный нами алгоритм и метаданные движений работают только с подчиненными регистрами (у которых в конфигураторе свойство «Режим записи» установлено в значение «Подчинение регистратору»). Независимые регистры сведений не привязываются к документу через стандартный реквизит Регистратор, поэтому они не будут отражены в метаданных движений документа. Если ваш документ делает записи в независимый регистр сведений программно, этот факт придется отслеживать отдельно.
2. Пустые наборы записей. Иногда при отмене проведения документа или при его перезаписи система записывает в таблицы движений пустые наборы. Физически в базе данных SQL записи очищаются. Наш оптимальный запрос отработает правильно: он не найдет строк и покажет, что движений нет. В то же время, если вы будете использовать объектный метод, Прочитать() вернет пустую коллекцию, и Количество() также покажет 0. Результаты совпадают, но запрос делает это в сотни раз быстрее.
3. Свойство «Записывать» в транзакции проведения. Если вам необходимо проанализировать движения документа непосредственно в процессе его проведения (например, внутри обработчика ОбработкаПроведения в модуле документа), выполнять запрос к базе данных может быть бесполезно, так как данные еще не записаны физически и не зафиксированы в СУБД. В этой специфической ситуации вам действительно придется обращаться напрямую к коллекции Движения.ИмяРегистра. Но помните, чтобы эти движения были сформированы в памяти во время проведения, у соответствующего набора записей должно быть установлено свойство Записывать = Истина.
Используйте правильные методы работы с платформой 1С:Предприятие 8, избегайте получения объектов без явной необходимости и всегда отдавайте предпочтение оптимизированным запросам перед объектным обходом коллекций! В задачах миграции между неидентичными конфигурациями для сопоставления объектов используется обработка для переноса данных между похожими конфигурациями 1С.