Как решить проблему циклических ссылок при обмене данными в 1С без использования GUID

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

При разработке правил обмена в среде «1С:Конвертация данных 2.1» одной из наиболее частых и архитектурно сложных задач является обработка циклических ссылок. В сообществе разработчиков 1С эта проблема закрепилась под метафоричным названием «проблема курицы и яйца».

Классический пример такой зависимости — пара объектов Контрагенты и БанковскиеСчета:

  1. У элемента справочника Контрагенты есть реквизит ОсновнойБанковскийСчет, ссылающийся на элемент справочника БанковскиеСчета.
  2. В то же время у элемента БанковскиеСчета есть обязательный реквизит Владелец, ссылающийся обратно на этого же Контрагента.

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

Проанализируем эту ситуацию и разберем четыре профессиональных метода решения этой проблемы — от простых декларативных настроек до сложных оптимизированных алгоритмов.

Способ 1. Декларативный метод с использованием двух ПКО (рекомендуемый)

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

Суть метода заключается в разделении логики создания контрагента на два этапа с помощью двух разных правил конвертации объектов (ПКО).

Выполним настройку по шагам:

  1. Создадим первое правило конвертации — Контрагенты_БезСчета. В настройках этого ПКО сопоставим ключевые поля поиска (ИНН, КПП, Наименование). Важно: полностью отключим или удалим из табличной части ПКС (правила конвертации свойств) выгрузку реквизита ОсновнойБанковскийСчет.
  2. Создадим второе правило конвертации — Контрагенты_СоСчетом. Настроим те же поля поиска, но в ПКС оставим выгрузку свойства ОсновнойБанковскийСчет по стандартному правилу БанковскиеСчета.
  3. Перейдем в ПКО БанковскиеСчета. Для свойства Владелец в колонке «Правило конвертации» принудительно укажем созданное нами первое правило — Контрагенты_БезСчета.
  4. В правилах выгрузки данных (ПВД) для выгрузки контрагентов укажем использование второго ПКО — Контрагенты_СоСчетом.

Выясним причину, почему это работает без сбоев:

Способ 2. Программный метод через передачу параметров и ручную запись

Рассмотрим вариант решения, близкий к тому, который реализовал автор темы на форуме. Этот подход полезен, когда логика синхронизации завязана на динамические условия.

В ПКО Контрагенты полностью отключим автоматическую выгрузку свойств ОсновнойБанковскийСчет и ОсновноеКонтактноеЛицо (сняв флаги активности в ПКС). Вместо этого перенесем выгрузку в обработчик события «После выгрузки в файл» у ПКО контрагента.

Пропишем следующий код в обработчике:


ОсновнойБанковскийСчет = Источник.ОсновнойБанковскийСчет;
Если ЗначениеЗаполнено(ОсновнойБанковскийСчет) Тогда
    // Формируем структуру исходящих данных для передачи флага-признака в подчиненный объект
    ПараметрыСчета = Новый Структура("ИмяРеквизитаДляЗаполнения", "ОсновнойБанковскийСчет");
    ВыгрузитьПоПравилу(ОсновнойБанковскийСчет, , ПараметрыСчета, , "БанковскиеСчета");
КонецЕсли;

В ПКО БанковскиеСчета добавим параметр ИмяРеквизитаДляЗаполнения. В обработчике ПКС для этого параметра в событии «Перед выгрузкой» напишем:


Если ТипЗнч(ИсходящиеДанные) = Тип("Структура") И ИсходящиеДанные.Свойство("ИмяРеквизитаДляЗаполнения") Тогда
    Значение = ИсходящиеДанные.ИмяРеквизитаДляЗаполнения;
Иначе
    Отказ = Истина; // Исключаем выгрузку параметра, если счет выгружается сам по себе, а не из контрагента
КонецЕсли;

Теперь на стороне приемника в обработчике события «После загрузки» ПКО БанковскиеСчета нам необходимо восстановить связь. Напишем алгоритм обратной записи:


Владелец = Объект.Владелец;
ИмяРеквизита = ПараметрыОбъекта["ИмяРеквизитаДляЗаполнения"];

Если ЗначениеЗаполнено(Владелец) И ЗначениеЗаполнено(ИмяРеквизита) Тогда
    СсылкаНаСчет = Ссылка; // Текущая ссылка загруженного банковского счета
    
    // Проверим, отличается ли текущее значение в базе от загружаемого
    Если Владелец[ИмяРеквизита] <> СсылкаНаСчет Тогда
        ВладелецОбъект = Владелец.ПолучитьОбъект();
        Если ВладелецОбъект <> Неопределено Тогда
            ВладелецОбъект[ИмяРеквизита] = СсылкаНаСчет;
            ВладелецОбъект.ОбменДанными.Загрузка = Истина;
            ВладелецОбъект.Записать();
        КонецЕсли;
    КонецЕсли;
КонецЕсли;

Способ 3. Расчетные детерминированные 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С распознает эти жесткие связи, и проблема циклических ссылок решится автоматически на уровне ядра СУБД.

Способ 4. Отложенная обработка через глобальные параметры (Оптимальный по скорости)

Предыдущие способы (особенно программный способ №2) имеют существенный недостаток — они заставляют платформу записывать объект Контрагент несколько раз во время разбора XML-файла. При больших объемах данных это существенно снижает скорость загрузки и повышает риск возникновения транзакционных блокировок.

Оптимальным решением будет сбор информации о связях во временную таблицу в памяти и ее пакетная обработка в самом конце загрузки.

Для этого в ПКО БанковскиеСчета в обработчике «После загрузки» мы не будем сразу записывать контрагента, а зафиксируем необходимость связи в глобальном параметре:


ТаблицаСвязей = Параметры.Получить("ТаблицаДляУстановкиОсновныхСчетов");
Если ТаблицаСвязей = Неопределено Тогда
    ТаблицаСвязей = Новый ТаблицаЗначений;
    ТаблицаСвязей.Колонки.Добавить("КонтрагентСсылка");
    ТаблицаСвязей.Колонки.Добавить("СчетСсылка");
    Параметры.Вставить("ТаблицаДляУстановкиОсновныхСчетов", ТаблицаСвязей);
КонецЕсли;

ИмяРеквизита = ПараметрыОбъекта["ИмяРеквизитаДляЗаполнения"];
Если ЗначениеЗаполнено(ИмяРеквизита) Тогда
    НоваяСтрока = ТаблицаСвязей.Добавить();
    НоваяСтрока.КонтрагентСсылка = Объект.Владелец;
    НоваяСтрока.СчетСсылка = Ссылка;
КонецЕсли;

Теперь перейдем в глобальные обработчики правил конвертации и найдем событие «После загрузки данных» (оно выполняется один раз, когда все объекты из файла уже прочитаны и созданы в базе данных). Напишем код для финального проведения связей:


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

Этот подход гарантирует максимальное быстродействие. Каждый контрагент запишется строго один раз, причем в тот момент, когда транзакции по созданию расчетных счетов уже успешно завершены.

← На главную