При разработке правил обмена в среде «1С:Конвертация данных 2.1» одной из наиболее частых и архитектурно сложных задач является обработка циклических ссылок. В сообществе разработчиков 1С эта проблема закрепилась под метафоричным названием «проблема курицы и яйца».
Классический пример такой зависимости — пара объектов Контрагенты и БанковскиеСчета:
Контрагенты есть реквизит ОсновнойБанковскийСчет, ссылающийся на элемент справочника БанковскиеСчета.БанковскиеСчета есть обязательный реквизит Владелец, ссылающийся обратно на этого же Контрагента.Если обмен выполняется нативно по уникальным идентификаторам (GUID/УИД), платформа 1С автоматически решает эту коллизию. При загрузке контрагента в поле основного счета записывается «битая» ссылка (объект не найден) с нужным GUID. Затем загружается сам счет с этим же GUID, и ссылка в контрагенте автоматически «оживает». Однако, если базы данных создавались копированием, переименовывались или велись независимо, сопоставление по GUID становится невозможным. Разработчикам приходится настраивать поиск по полям поиска (например, ИНН + КПП для контрагента и Номер счета + БИК для банка). В этот момент стандартный механизм платформы ломается: приемник не может создать ссылку на счет, пока тот физически не записан в базу, а записать счет нельзя, так как еще не создан его владелец.
Проанализируем эту ситуацию и разберем четыре профессиональных метода решения этой проблемы — от простых декларативных настроек до сложных оптимизированных алгоритмов.
Этот метод считается эталонным, поскольку он настраивается исключительно в визуальном редакторе «Конвертации данных» и не требует написания сложного программного кода в обработчиках событий.
Суть метода заключается в разделении логики создания контрагента на два этапа с помощью двух разных правил конвертации объектов (ПКО).
Выполним настройку по шагам:
Контрагенты_БезСчета. В настройках этого ПКО сопоставим ключевые поля поиска (ИНН, КПП, Наименование). Важно: полностью отключим или удалим из табличной части ПКС (правила конвертации свойств) выгрузку реквизита ОсновнойБанковскийСчет.Контрагенты_СоСчетом. Настроим те же поля поиска, но в ПКС оставим выгрузку свойства ОсновнойБанковскийСчет по стандартному правилу БанковскиеСчета.БанковскиеСчета. Для свойства Владелец в колонке «Правило конвертации» принудительно укажем созданное нами первое правило — Контрагенты_БезСчета.Контрагенты_СоСчетом.Выясним причину, почему это работает без сбоев:
Контрагенты_СоСчетом.Владельца. Система обращается к правилу Контрагенты_БезСчета. По полям поиска (ИНН/КПП) проверяется наличие контрагента в базе. Если его нет, он создается и записывается (но без заполненного поля основного счета, благодаря чему зацикливание прерывается).ОсновнойБанковскийСчет контрагента.Рассмотрим вариант решения, близкий к тому, который реализовал автор темы на форуме. Этот подход полезен, когда логика синхронизации завязана на динамические условия.
В ПКО Контрагенты полностью отключим автоматическую выгрузку свойств ОсновнойБанковскийСчет и ОсновноеКонтактноеЛицо (сняв флаги активности в ПКС). Вместо этого перенесем выгрузку в обработчик события «После выгрузки в файл» у ПКО контрагента.
Пропишем следующий код в обработчике:
ОсновнойБанковскийСчет = Источник.ОсновнойБанковскийСчет;
Если ЗначениеЗаполнено(ОсновнойБанковскийСчет) Тогда
// Формируем структуру исходящих данных для передачи флага-признака в подчиненный объект
ПараметрыСчета = Новый Структура("ИмяРеквизитаДляЗаполнения", "ОсновнойБанковскийСчет");
ВыгрузитьПоПравилу(ОсновнойБанковскийСчет, , ПараметрыСчета, , "БанковскиеСчета");
КонецЕсли;
В ПКО БанковскиеСчета добавим параметр ИмяРеквизитаДляЗаполнения. В обработчике ПКС для этого параметра в событии «Перед выгрузкой» напишем:
Если ТипЗнч(ИсходящиеДанные) = Тип("Структура") И ИсходящиеДанные.Свойство("ИмяРеквизитаДляЗаполнения") Тогда
Значение = ИсходящиеДанные.ИмяРеквизитаДляЗаполнения;
Иначе
Отказ = Истина; // Исключаем выгрузку параметра, если счет выгружается сам по себе, а не из контрагента
КонецЕсли;
Теперь на стороне приемника в обработчике события «После загрузки» ПКО БанковскиеСчета нам необходимо восстановить связь. Напишем алгоритм обратной записи:
Владелец = Объект.Владелец;
ИмяРеквизита = ПараметрыОбъекта["ИмяРеквизитаДляЗаполнения"];
Если ЗначениеЗаполнено(Владелец) И ЗначениеЗаполнено(ИмяРеквизита) Тогда
СсылкаНаСчет = Ссылка; // Текущая ссылка загруженного банковского счета
// Проверим, отличается ли текущее значение в базе от загружаемого
Если Владелец[ИмяРеквизита] <> СсылкаНаСчет Тогда
ВладелецОбъект = Владелец.ПолучитьОбъект();
Если ВладелецОбъект <> Неопределено Тогда
ВладелецОбъект[ИмяРеквизита] = СсылкаНаСчет;
ВладелецОбъект.ОбменДанными.Загрузка = Истина;
ВладелецОбъект.Записать();
КонецЕсли;
КонецЕсли;
КонецЕсли;
Если циклическая зависимость — не единственная проблема, а базы сильно рассинхронизированы, можно применить продвинутый архитектурный лайфхак. Мы можем виртуально перевести обмен с «полей поиска» на «поиск по GUID», рассчитывая эти идентификаторы прямо в процессе выгрузки на основе ключевых бизнес-полей.
Для этого в ПКО Контрагенты в обработчике «Перед выгрузкой» сформируем стабильный GUID из ИНН и КПП контрагента:
СтрокаКлюча = СокрЛП(Источник.ИНН) + "_" + СокрЛП(Источник.КПП);
// Используем алгоритм хеширования (например, MD5) для получения уникальной 32-символьной строки
ХэшСтрока = ПолучитьХэшMD5(СтрокаКлюча);
// Преобразуем хэш-строку в стандартный формат GUID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
СтрокаGUID = Сред(ХэшСтрока, 1, 8) + "-" +
Сред(ХэшСтрока, 9, 4) + "-" +
Сред(ХэшСтрока, 13, 4) + "-" +
Сред(ХэшСтрока, 17, 4) + "-" +
Сред(ХэшСтрока, 21, 12);
УИД = Новый УникальныйИдентификатор(СтрокаGUID);
// Подменяем ссылку выгружаемого объекта новой ссылкой с расчетным GUID
Значение = Справочники.Contragents.ПолучитьСсылку(УИД);
Аналогичную генерацию применим для банковских счетов, используя связку БИК банка + Номер счета. При импорте платформа 1С распознает эти жесткие связи, и проблема циклических ссылок решится автоматически на уровне ядра СУБД.
Предыдущие способы (особенно программный способ №2) имеют существенный недостаток — они заставляют платформу записывать объект Контрагент несколько раз во время разбора XML-файла. При больших объемах данных это существенно снижает скорость загрузки и повышает риск возникновения транзакционных блокировок.
Оптимальным решением будет сбор информации о связях во временную таблицу в памяти и ее пакетная обработка в самом конце загрузки.
Для этого в ПКО БанковскиеСчета в обработчике «После загрузки» мы не будем сразу записывать контрагента, а зафиксируем необходимость связи в глобальном параметре:
ТаблицаСвязей = Параметры.Получить("ТаблицаДляУстановкиОсновныхСчетов");
Если ТаблицаСвязей = Неопределено Тогда
ТаблицаСвязей = Новый ТаблицаЗначений;
ТаблицаСвязей.Колонки.Добавить("КонтрагентСсылка");
ТаблицаСвязей.Колонки.Добавить("СчетСсылка");
Параметры.Вставить("ТаблицаДляУстановкиОсновныхСчетов", ТаблицаСвязей);
КонецЕсли;
ИмяРеквизита = ПараметрыОбъекта["ИмяРеквизитаДляЗаполнения"];
Если ЗначениеЗаполнено(ИмяРеквизита) Тогда
НоваяСтрока = ТаблицаСвязей.Добавить();
НоваяСтрока.КонтрагентСсылка = Объект.Владелец;
НоваяСтрока.СчетСсылка = Ссылка;
КонецЕсли;
Теперь перейдем в глобальные обработчики правил конвертации и найдем событие «После загрузки данных» (оно выполняется один раз, когда все объекты из файла уже прочитаны и созданы в базе данных). Напишем код для финального проведения связей:
ТаблицаСвязей = Параметры.Получить("ТаблицаДляУстановкиОсновныхСчетов");
Если ТаблицаСвязей <> Неопределено Тогда
Для Каждого СтрокаСвязи Из ТаблицаСвязей Цикл
Если ЗначениеЗаполнено(СтрокаСвязи.КонтрагентСсылка) И ЗначениеЗаполнено(СтрокаСвязи.СчетСсылка) Тогда
КонтрагентОбъект = СтрокаСвязи.КонтрагентСсылка.ПолучитьОбъект();
Если КонтрагентОбъект <> Неопределено Тогда
// Заполняем реквизит только если он отличается
Если КонтрагентОбъект.ОсновнойБанковскийСчет <> СтрокаСвязи.СчетСсылка Тогда
КонтрагентОбъект.ОсновнойБанковскийСчет = СтрокаСвязи.СчетСсылка;
КонтрагентОбъект.ОбменДанными.Загрузка = Истина;
КонтрагентОбъект.Записать();
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецДляЦикла;
КонецЕсли;
Этот подход гарантирует максимальное быстродействие. Каждый контрагент запишется строго один раз, причем в тот момент, когда транзакции по созданию расчетных счетов уже успешно завершены.