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