Перенос данных из одной системы в другую (например, из УПП 1.3 в ERP 2.5) часто сопряжен с необходимостью вычисления параметров «на лету». Типичная ситуация: в источнике (УПП) счета учета номенклатуры хранятся в регистре сведений СчетаУчетаНоменклатуры. При этом настройки могут быть заданы как для конкретной номенклатуры, так и для её родителя или родителя родителя (до 6 уровней вложенности). Попытка получить эти данные через прямой запрос внутри обработчика ПКО (Правила конвертации объектов) приводит к катастрофическому падению производительности из-за «запросов в цикле».
В этой статье мы подробно разберем, как исправить «грех» запросов в цикле и сделать миграцию данных быстрой и профессиональной. Мы рассмотрим несколько подходов: от микро-оптимизации до использования кэширования и типовых механизмов конфигурации.
Если объем данных невелик и вы хотите оставить текущую логику, первым делом необходимо вынести создание объекта Запрос и определение его текста за пределы итераций. Рассмотрим, почему это важно: создание нового объекта Запрос в каждой итерации заставляет систему выделять память и повторно компилировать текст запроса, что крайне неэффективно.
Проанализируем пример кода, который позволит немного ускорить процесс без кардинальной переработки алгоритма:
// Создаем объект один раз перед началом поиска
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СчетаУчетаНоменклатуры.СчетУчетаБУ КАК СЧ,
| СчетаУчетаНоменклатуры.СчетДоходовОтРеализацииБУ КАК СЧД
|ИЗ
| РегистрСведений.СчетаУчетаНоменклатуры КАК СчетаУчетаНоменклатуры
|ГДЕ
| СчетаУчетаНоменклатуры.Номенклатура = &Номенклатура";
// Основной цикл поиска по иерархии
ЭлОтбора = Источник.Ссылка;
Счет = Неопределено;
СчИтераций = 1;
Пока ЭлОтбора <> Справочники.Номенклатура.ПустаяСсылка() И СчИтераций <= 7 Цикл
Запрос.УстановитьПараметр("Номенклатура", ЭлОтбора);
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Если Выборка.Следующий() Тогда
Счет = Выборка.СЧ;
Если ЗначениеЗаполнено(Счет) Тогда
Прервать; // Счет найден, выходим из цикла
КонецЕсли;
КонецЕсли;
// Переходим к родителю для следующей попытки
ЭлОтбора = ЭлОтбора.Родитель;
СчИтераций = СчИтераций + 1;
КонецЦикла;
Важный момент: обязательно добавьте условие Прервать, как только значение найдено, и проверку на ПустаяСсылка, чтобы избежать лишних обращений к базе, когда иерархия закончилась.
Это «золотой стандарт» для Конвертации данных. Вместо того чтобы каждый раз ходить в базу за данными конкретной номенклатуры, мы один раз выгружаем весь регистр в память и работаем с ним. Рассмотрим, как реализовать это по шагам.
Шаг 1. Подготовка кэша (Обработчик «Перед выгрузкой данных» в ПВД или Глобальный обработчик)
В этом обработчике мы создаем таблицу, которая будет хранить все актуальные настройки счетов.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СчетаУчетаНоменклатуры.Номенклатура КАК Номенклатура,
| СчетаУчетаНоменклатуры.СчетУчетаБУ КАК Счет
|ИЗ
| РегистрСведений.СчетаУчетаНоменклатуры КАК СчетаУчетаНоменклатуры";
ТЗ_Счета = Запрос.Выполнить().Выгрузить();
ТЗ_Счета.Индексировать("Номенклатура");
// Помещаем таблицу в параметры конвертации, чтобы она была доступна везде
Параметры.Вставить("КэшСчетовНоменклатуры", ТЗ_Счета);
Шаг 2. Использование кэша в ПКО (Событие «Перед выгрузкой» для реквизита)
Теперь вместо Запрос.Выполнить() мы используем поиск по индексированной таблице значений. Это работает в десятки раз быстрее.
Кэш = Параметры.КэшСчетовНоменклатуры;
ТекущийОбъект = Источник.Ссылка;
НайденныйСчет = Неопределено;
Для Итерация = 1 По 7 Цикл
Если ТекущийОбъект.Пустая() Тогда Прервать; КонецЕсли;
// Ищем строку в кэше
СтрокаКэша = Кэш.Найти(ТекущийОбъект, "Номенклатура");
Если СтрокаКэша <> Неопределено И ЗначениеЗаполнено(СтрокаКэша.Счет) Тогда
НайденныйСчет = СтрокаКэша.Счет;
Прервать;
КонецЕсли;
ТекущийОбъект = ТекущийОбъект.Родитель;
КонецЦикла;
Значение = НайденныйСчет;
Для еще большего ускорения вместо ТаблицыЗначений можно использовать объект Соответствие (Map), так как поиск в нем по ключу (ссылке) является максимально быстрым.
Если мы заранее знаем, что глубина иерархии ограничена (в нашем случае 6-7 уровней), мы можем получить итоговый счет за один запрос для каждого элемента, не используя циклы 1С вовсе. Мы «развернем» иерархию в одну строку с помощью левых соединений таблицы номенклатуры самой с собой.
Проанализируем структуру такого запроса:
ВЫБРАТЬ
ЕСТЬNULL(СчетаУр0.СчетУчетаБУ,
ЕСТЬNULL(СчетаУр1.СчетУчетаБУ,
ЕСТЬNULL(СчетаУр2.СчетУчетаБУ,
ЕСТЬNULL(СчетаУр3.СчетУчетаБУ, "Не определено")))) КАК ИтоговыйСчет
ИЗ
Справочник.Номенклатура КАК Н_Элемент
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СчетаУчетаНоменклатуры КАК СчетаУр0
ПО Н_Элемент.Ссылка = СчетаУр0.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СчетаУчетаНоменклатуры КАК СчетаУр1
ПО Н_Элемент.Родитель = СчетаУр1.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СчетаУчетаНоменклатуры КАК СчетаУр2
ПО Н_Элемент.Родитель.Родитель = СчетаУр2.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СчетаУчетаНоменклатуры КАК СчетаУр3
ПО Н_Элемент.Родитель.Родитель.Родитель = СчетаУр3.Номенклатура
ГДЕ
Н_Элемент.Ссылка = &Ссылка
Функция ЕСТЬNULL (ISNULL) здесь работает как фильтр приоритетов: если на уровне самого элемента счета нет, берется значение родителя, и так далее до самого верха. Это решение позволяет серверу базы данных выполнить всю работу за один проход.
Разработчики типовых конфигураций (особенно в УПП) уже сталкивались с этой задачей. В общем модуле УправлениеЗапасами или СпискиОбъектовУзловОтраженияВУчете (в зависимости от версии) часто существуют экспортные функции, предназначенные для пакетного получения счетов учета.
Рассмотрим пример вызова такой функции:
// В УПП часто используется подобный механизм
МассивНоменклатуры = Новый Массив;
МассивНоменклатуры.Добавить(Источник.Ссылка);
// Вызов типовой функции, которая умеет искать счета с учетом иерархии
ТаблицаСчетов = УправлениеЗапасами.ПолучитьСчетаУчетаНоменклатуры(МассивНоменклатуры, Организация);
Если ТаблицаСчетов.Количество() > 0 Тогда
Счет = ТаблицаСчетов[0].СчетУчетаБУ;
КонецЕсли;
Преимущество: типовые функции обычно максимально оптимизированы (используют временные таблицы и пакетные запросы).
Недостаток: если вы делаете выгрузку на стороне ERP (подключаясь к УПП через COM), вызов серверных функций может быть медленным. В этом случае лучше использовать Вариант 2.
Если база огромная и Конвертация данных не справляется, воспользуемся советом опытных архитекторов: добавим в справочник Номенклатура в УПП временный дополнительный реквизит (или воспользуемся механизмом свойств). Перед выгрузкой запустим простую обработку, которая один раз вычислит счета для всех позиций и запишет их в этот реквизит.
Тогда в правилах конвертации (ПКО) вам не нужно будет писать никакого кода: реквизит СчетУчета_ДляВыгрузки просто сопоставится с нужным полем в ERP. Это самый стабильный и быстрый метод для «тяжелых» миграций.
Подведем итоги нашего анализа:
Запрос за цикл и добавить Прервать.Параметры.КэшСчетовНоменклатуры через Таблицу Значений или Соответствие. Это самый универсальный метод в КД 2.0.УправлениеЗапасами.ПолучитьСчетаУчетаНоменклатуры, так как она корректно обрабатывает все нюансы (склады, организации).ГруппыФинансовогоУчетаНоменклатуры.Использование любого из этих методов вместо запроса в цикле поднимет ваш уровень как разработчика и сэкономит часы времени при выполнении миграции.