1C Config Merger v2: Dependency-Driven алгоритм слияния конфигураций — Блог
$ cat 1c-autounion-algorythm-research-v2-dependency-merger.md

1C Config Merger v2: Dependency-Driven алгоритм слияния конфигураций

1C Config Merger v2: Dependency-Driven алгоритм слияния

В первой статье мы описали начальный подход к автоматизации объединения конфигураций 1С. Он работал, но был неэффективен для больших конфигураций. Сегодня расскажем, как мы полностью переработали алгоритм.

Проблема v1

Наш первый подход был простым:

1. Распарсить ВСЕ объекты ERP (~40 000)
2. Распарсить ВСЕ объекты APK (~500)
3. Сравнить всё со всем (O(N×M))
4. Слить параллельно

Проблемы:

  • Загружаем 40 000+ объектов в память
  • Не учитываем зависимости между объектами
  • Сложно отлаживать (всё сразу)
  • Подсистемы обрабатываются постфактум

Идея v2: Dependency-Driven подход

Вместо того чтобы обрабатывать все объекты, мы идём от меньшей конфигурации к большей, рекурсивно разрешая зависимости.

1. Берём APK (500 объектов)
2. Находим modified-файлы (без префикса) — их нужно мерджить
3. Мерджим по одному файлу
4. Извлекаем ссылки из результата
5. Если ссылка на объект из APK — добавляем в очередь
6. Если ссылка на объект из ERP — тоже добавляем
7. Повторяем пока очередь не пуста
8. Добавляем new-файлы (с префиксом), привязывая к подсистемам
9. Копируем неизменённые объекты из ERP

Преимущества:

  • Ленивая загрузка — только нужные объекты
  • Dependency-aware — порядок определяется графом ссылок
  • Отладка — по одному файлу, понятно что происходит
  • Подсистемы — в процессе, а не после

Алгоритм v2

Шаг 1: Классификация объектов APK

modified_files = []  # без префикса — нужно мерджить
new_files = []       # с префиксом — нужно добавить

for obj in apk.objects:
    if obj.name.startswith("Апк"):
        new_files.append(obj)
    else:
        modified_files.append(obj)

Статистика по APK:

Тип объектовВсегоModified (без префикса)New (с префиксом)
CommonModules306246 (80%)60 (20%)
Catalogs260100 (38%)160 (62%)
Documents259123 (48%)136 (52%)

Шаг 2: Iterative merge с очередью

visited = set()          # уже обработанные UUID
queue = MergeQueue()     # приоритетная очередь

# Заполняем очередь modified-файлами (приоритет 0)
for f in modified_files:
    queue.push(f, priority=0)

while not queue.empty():
    ext_obj = queue.pop()
    
    if ext_obj.uuid in visited:
        continue
    visited.add(ext_obj.uuid)
    
    # Находим пару в ERP
    base_obj = erp.get_by_uuid(ext_obj.uuid) or erp.get_by_name(ext_obj.name)
    
    if base_obj:
        # Мерджим
        merged = merge_objects(base_obj, ext_obj)
        merged_config.add(merged)
        
        # Извлекаем ссылки из merged объекта
        refs = extract_all_references(merged)
        
        for ref in refs:
            ref_in_apk = apk.get_by_name(ref)
            ref_in_erp = erp.get_by_name(ref)
            
            # Если ссылка на объект из APK — в очередь
            if ref_in_apk and ref_in_apk.uuid not in visited:
                queue.push(ref_in_apk, priority=2)
            
            # Если ссылка на объект из ERP — тоже в очередь
            if ref_in_erp and ref_in_erp.uuid not in visited:
                queue.push(ref_in_erp, priority=2)

Шаг 3: Добавление новых объектов

for new_obj in new_files:
    if new_obj.uuid not in visited:
        merged_config.add(new_obj)
        
        # Привязываем к подсистеме
        subsystem = find_subsystem(new_obj, apk)
        if subsystem:
            subsystem.content.add(new_obj)

Шаг 4: Копирование неизменённых

for base_obj in erp.objects:
    if base_obj.uuid not in visited:
        merged_config.add(base_obj)

Граф зависимостей

Ключевой компонент v2 — DependencyGraph:

class DependencyGraph:
    nodes: {uuid: MetadataObject}
    edges: {uuid: [ref_uuid, ...]}
    
    def get_dependencies(uuid) → [uuid]   # от кого зависит
    def get_dependents(uuid) → [uuid]     # кто зависит от него
    def topological_sort() → [uuid]       # порядок обработки
    def detect_cycles() → [[uuid, ...]]   # циклы!

Пример графа

CommonModules.БухгалтерскиеОтчеты
    ↓ ссылается
Catalog.АпкСтатьиРасходовЕСХН  (NEW)
    ↓ ссылается
Enum.АпкВидыРасходов  (NEW)
    ↓ ссылается
Catalog.Организации  (MODIFIED, есть в ERP)

Порядок обработки (topological sort):

  1. Enum.АпкВидыРасходов — сначала, ни от кого не зависит
  2. Catalog.АпкСтатьиРасходовЕСХН — зависит от Enum
  3. CommonModules.БухгалтерскиеОтчеты — зависит от Catalog
  4. Catalog.Организации — мерджим, т.к. на него ссылаются

Циклические зависимости

В 1С они встречаются:

Module A → ссылается на Catalog B
Catalog B → ссылается на Document C
Document C → ссылается на Module A  ← цикл!

Обработка: visited set предотвращает бесконечную рекурсию. Цикл логируется, но не блокирует слияние.


Извлечение ссылок

Самая сложная часть — найти все ссылки в объекте.

Из XML (MDObjectRef)

<xr:Item xsi:type="xr:MDObjectRef">Catalog.Валюты</xr:Item>
<xr:Item xsi:type="xr:MDObjectRef">Document.АвансовыйОтчет</xr:Item>
pattern = r'<xr:Item xsi:type="xr:MDObjectRef">([^<]+)</xr:Item>'
# → "Catalog.Валюты" → ("Catalog", "Валюты")

Из BSL-кода

// Прямые обращения к объектам
Справочники.АпкПоля.НайтиПоКоду(код)
Документы.АпкПланированиеРабот.СоздатьДокумент()
Перечисления.АпкВидыПериода.ЗаМесяц

// Через запросы
Запрос.Текст = "ВЫБРАТЬ ... ИЗ Документ.АпкПланированиеРабот ..."

// Через предопределенные значения
ПредопределенноеЗначение("Перечисление.АпкВидыПериода.ЗаМесяц")
patterns = [
    r'Справочники\.(\w+)',           # → Catalog
    r'Документы\.(\w+)',             # → Document
    r'Перечисления\.(\w+)',          # → Enum
    r'РегистрыСведений\.(\w+)',      # → InformationRegister
    r'РегистрыНакопления\.(\w+)',    # → AccumulationRegister
    r'Документ\.(\w+)',              # → Document (в запросах)
    r'Справочник\.(\w+)',            # → Catalog (в запросах)
    r'Перечисление\.(\w+)',          # → Enum (в запросах)
]

Из метаданных (Type в XML)

<Type>
    <v8:Type>cfg:CatalogRef.АпкСтатьиРасходовЕСХН</v8:Type>
</Type>
pattern = r'cfg:(CatalogRef|DocumentRef|EnumRef)\.(\w+)'
# → ("CatalogRef", "АпкСтатьиРасходовЕСХН")

Из форм

<DataPath>Объект.АпкСтатьяРасходов</DataPath>
pattern = r'Объект\.(\w+)'
# → "АпкСтатьяРасходов"

Привязка к подсистемам

Новые объекты (с префиксом) должны быть привязаны к подсистемам:

&lt;Subsystem uuid="..."&gt;
    &lt;Name&gt;АпкРастениеводство&lt;/Name&gt;
    &lt;Content&gt;
        &lt;xr:Item xsi:type="xr:MDObjectRef"&gt;Catalog.АпкПоля&lt;/xr:Item&gt;
        &lt;xr:Item xsi:type="xr:MDObjectRef"&gt;Document.АпкПланированиеРабот&lt;/xr:Item&gt;
        &lt;xr:Item xsi:type="xr:MDObjectRef"&gt;Enum.АпкВидыРабот&lt;/xr:Item&gt;
    &lt;/Content&gt;
    &lt;ChildObjects&gt;
        &lt;Subsystem&gt;АпкРаботыВРастениеводстве&lt;/Subsystem&gt;
    &lt;/ChildObjects&gt;
&lt;/Subsystem&gt;

Алгоритм привязки:

def bind_to_subsystem(new_obj, apk_config):
    """Найти подсистему, которой принадлежит объект"""
    for subsystem in apk_config.get_subsystems():
        if new_obj.name in subsystem.content:
            return subsystem
    return None  # Объект без подсистемы → в "Общие"

Сравнение v1 и v2

Критерийv1 (Parallel)v2 (Dependency-Driven)
ЗагрузкаВсе 40K объектовТолько нужные
ПорядокСлучайныйTopological sort
ЗависимостиНе учитываютсяРекурсивно разрешаются
ОтладкаСложноПо одному файлу
ПодсистемыПостфактумВ процессе
ПамятьO(N + M)O(K + R), K << N
ВремяO(N×M)O(K × R), K << N

Где N = 40 000 (ERP), M = 500 (APK), K = modified (~300), R = avg references (~10).

v2 обрабатывает ~3 000 операций против ~20 000 000 в v1.


Архитектура v2

src/
├── models/
│   ├── dependency_graph.py    # NEW: граф зависимостей
│   ├── merge_queue.py         # NEW: приоритетная очередь
│   └── config_object.py       # из v1

├── parser/
│   ├── reference_extractor.py # NEW: извлечение ссылок
│   └── ...                    # из v1

├── merger/
│   ├── merge_engine_v2.py     # NEW: dependency-driven цикл
│   ├── subsystem_binder.py    # NEW: привязка к подсистемам
│   └── ...                    # из v1

Что дальше

Следующий шаг — реализация и интеграционное тестирование:

  1. Реализовать DependencyGraph + MergeQueue
  2. Реализовать ReferenceExtractor (XML + BSL)
  3. Реализовать MergeEngine v2 (основной цикл)
  4. Реализовать SubsystemBinder
  5. Написать интеграционный тест: ERP + APK → сравнить с APK+ERP

Если результат совпадёт с эталоном хотя бы на 95% — можно говорить о готовности инструмента.