Legacy логика vs MPTT: Руководство по навигации в иерархии¶
Содержание¶
- Введение
- Историческая эволюция
- Проблема инверсии
- Подходы к навигации
- Где используется legacy логика
- Где используется MPTT напрямую
- Рекомендации для нового кода
1. Введение¶
В проекте существуют два противоположных подхода к навигации по иерархии:
- Legacy логика (основная) - навигация "вниз" через поле child
- MPTT логика (добавлена позже) - стандартная MPTT навигация
Критическая проблема: Из-за parent_attr="child" эти подходы дают противоположные результаты!
2. Историческая эволюция¶
2.1. ДО MPTT (Legacy логика)¶
Изначально проект использовал простую FK-навигацию:
class Level(models.Model):
child = models.ForeignKey('Level', ...) # Ссылка на ребёнка
parents = models.ManyToManyField('Level', ...) # Родители
Концепция: Иерархия строится "вниз" от родителей к детям.
Пример:
brand → product → dpu
(brand.child = product означает "product - ребёнок brand")
2.2. Добавление MPTT¶
Для оптимизации запросов добавили django-mptt:
class Level(MPTTModel):
child = TreeForeignKey('Level', ...) # Существующее поле!
class MPTTMeta:
parent_attr = "child" # Используем существующее поле
Проблема: MPTT требует, чтобы FK указывал на родителя, но поле называется child!
Решение: Указали parent_attr="child", чтобы MPTT использовал это поле как ссылку на родителя.
3. Проблема инверсии¶
3.1. Что произошло¶
Из-за parent_attr="child" MPTT интерпретирует иерархию наоборот:
# Legacy логика:
brand.child = product # "product - ребёнок brand"
# MPTT интерпретация:
brand.child = product # "product - РОДИТЕЛЬ brand" (!)
3.2. Последствия¶
MPTT методы возвращают противоположное тому, что ожидается в legacy логике:
Структура (legacy):
brand → product → dpu
category → product
MPTT видит наоборот:
dpu → product → brand
↓
category
Методы:
| MPTT метод | Что возвращает (MPTT) | Что это в legacy логике |
|---|---|---|
product.get_children() |
[brand, category] |
РОДИТЕЛИ (не дети!) |
product.get_descendants() |
[brand, segment, category, ...] |
ПРЕДКИ (не потомки!) |
brand.get_ancestors() |
[dpu, product] |
ДЕТИ/ВНУКИ (не предки!) |
4. Подходы к навигации¶
4.1. Legacy подход (рекомендуемый)¶
Используйте обёртки и поля:
# ✅ Получить родителей
parents = level.get_parents_with_self() # [self] + родители
# ✅ Получить всех предков
ancestors = level.get_all_parents_with_self() # [self] + все предки
# ✅ Получить ребёнка
child_level = level.child
# ✅ Получить родителей (M2M)
parent_levels = level.parents.all()
Реализация обёрток:
def get_parents_with_self(self):
"""Возвращает родителей + self (legacy логика)"""
# Legacy логика противоположна MPTT: get_children() → родители
parents_new = list(self.get_children())
return [self] + parents_new
def get_all_parents_with_self(self):
"""Возвращает всех предков + self (legacy логика)"""
# Legacy логика противоположна MPTT: get_descendants() → предки
ancestors_new = [self] + list(self.get_descendants())
return ancestors_new
4.2. MPTT подход (требует понимания инверсии)¶
Используйте MPTT методы напрямую только с явным комментарием:
# Legacy логика противоположна MPTT: get_children() возвращает родителей в legacy смысле
parent_levels = list(level.get_children())
# Legacy логика противоположна MPTT: get_descendants() возвращает предков в legacy смысле
ancestor_levels = list(level.get_descendants())
5. Где используется legacy логика¶
5.1. Использование обёрток get_parents_with_self()¶
Production код (15+ мест):
core/hierarchy/libs/sync.py:102, 107, 112- синхронизация связей parentscore/hierarchy/libs/import_hierarchy_items.py:863, 1118, 1255- импорт иерархииcore/hierarchy/libs/queries.py:193, 234, 273- построение запросовcore/hierarchy/libs/generate_fake_hierarchy_items.py:105- генерация данныхcore/hierarchy/views.py:349- отображение в админкеapps/ibp0/scripts/forecast_level_settings.py:49- настройки прогнозаapps/ibp1/scripts/forecast_level_settings.py:49- настройки прогноза
5.2. Использование обёрток get_all_parents_with_self()¶
Production код (10+ мест):
core/hierarchy/models.py:576- методupdate_ancestors()core/hierarchy/models.py:584, 596, 600- методis_parent_for()core/hierarchy/models.py:614, 637- создание dim-таблицcore/reports/libs/data_request.py:1256- формирование отчётовcore/hierarchy/libs/queries.py:207- построение запросов к dim-таблицамcore/filters/data_request.py:17- фильтрация данныхcore/filters/libs/filter.py:830- кэширование уровнейcore/parameters/libs/data_transfer.py:62, 64- трансфер данныхcore/parameters/libs/fact_manager.py:1713- управление фактами
5.3. Использование поля child¶
Production код (4 места):
core/hierarchy/models.py:164- построение mermaid диаграммыcore/hierarchy/libs/sync.py:143, 164, 174- рекурсивная синхронизация "вниз"
# Пример из sync.py:
if with_children and level.child is not None and drop:
# Синхронизация дочерних уровней (идём "вниз" к ребёнку)
sync(level.child, ...)
5.4. Использование поля parents (M2M)¶
Production код (15+ мест):
core/hierarchy/libs/sync.py- работа со связями parentscore/hierarchy/libs/import_hierarchy_items.py- импорт родительских связейcore/hierarchy/libs/queries.py- построение запросов по родителям- И другие (см. полный список в разделе 5.1)
5.5. Legacy подход в Direct Pipeline (механизм удаления)¶
Файл: core/hierarchy/libs/direct/record_cleaner.py
Метод nullify_references_in_children() (строки ~320-370):
Находит все дочерние уровни (те, у кого удаляемый level в родителях) через перебор + проверку:
# ✅ Legacy подход: перебираем все уровни
all_levels = LevelModel.objects.filter(type=level.type)
child_levels = []
for potential_child in all_levels:
parents = potential_child.get_parents_with_self()
if level in parents and potential_child.id != level.id:
child_levels.append(potential_child)
Почему не используются прямые ссылки:
- Level.objects.filter(parents=level) - M2M parents не синхронизируется с FK child
- level.mptt_parents.all() - related_name работает в обратную сторону
Метод delete_from_full_cascade() (строки ~215-300):
Находит все уровни-потомки (те, у кого удаляемый level в предках) через перебор + проверку:
# ✅ Legacy подход: перебираем все уровни
all_levels = LevelModel.objects.filter(type=level.type)
all_descendant_levels = []
for potential_descendant in all_levels:
if potential_descendant.id == level.id:
continue
ancestors = potential_descendant.get_all_parents_with_self()
if level in ancestors:
all_descendant_levels.append(potential_descendant)
Почему не используется level.get_all_parents_with_self() напрямую:
- Метод возвращает ПРЕДКОВ level, а нам нужны ПОТОМКИ
- Перебор с проверкой - единственный надёжный способ в legacy логике
6. Где используется MPTT напрямую¶
6.1. Использование get_children() напрямую¶
В models.py (обёртки):
- models.py:538 - внутри get_parents_with_self() с комментарием "In MPTT terms parents are children"
В Direct Pipeline (новый код):
- dim_table_full.py:410, 568 - синхронизация (с комментариями о legacy логике)
- item_bridge.py:194 - синхронизация с Item (с комментарием)
- table_builder.py:256, 379 - построение таблиц (с комментарием)
- importer.py:352 - импорт данных (с комментарием)
В legacy коде:
- sync.py:631 - синхронизация уровней
- admin.py:280 - админка
- forms.py:109, 149, 180, 214 - формы
- import_hierarchy_items.py:442, 719, 1144 - импорт
- scripts/sync_level_sql.py:127 - SQL синхронизация
- build_calendar.py:252 - построение календаря
- И другие (всего ~28 мест)
6.2. Использование get_descendants() напрямую¶
В models.py (обёртки):
- models.py:558 - внутри get_all_parents_with_self() с комментарием "In MPTT terms parents are children"
В legacy коде:
- workflow/models.py:599 - проверка пересечений уровней
- authcustom/libs/extract_from_template.py:87 - проверка пересечений
6.3. Использование get_ancestors() напрямую¶
В legacy коде:
- workflow/models.py:620 - проверка пересечений
- authcustom/libs/extract_from_template.py:104 - проверка пересечений
Примечание: В record_cleaner.py ранее использовался get_ancestors() напрямую, но был исправлен на legacy подход (см. раздел 5.5).
7. Рекомендации для нового кода¶
7.1. ЧТО ДЕЛАТЬ¶
✅ Используйте legacy подход:
# Для получения родителей
parents = level.get_parents_with_self()
# Для получения всех предков
ancestors = level.get_all_parents_with_self()
# Для получения ребёнка
child = level.child
# Для получения родителей (M2M)
parent_list = level.parents.all()
# Для синхронизации M2M ancestors
level.update_ancestors()
7.2. ЧТО НЕ ДЕЛАТЬ¶
❌ Не используйте MPTT методы без понимания инверсии:
# ❌ Неправильно (без комментария)
children = level.get_children()
# ❌ Неправильно (без комментария)
descendants = level.get_descendants()
7.3. ИСКЛЮЧЕНИЯ¶
Допустимо использовать MPTT напрямую с явным комментарием:
# ✅ Правильно (с комментарием)
# Legacy логика противоположна MPTT: get_children() возвращает родителей в legacy смысле
parent_levels = list(level.get_children())
7.4. Методы, которые работают одинаково¶
Эти MPTT методы можно использовать без оговорок:
# ✅ Братья/сёстры (работает одинаково)
siblings = level.get_siblings()
# ✅ Корень дерева (работает одинаково)
root = level.get_root()
# ✅ Проверки (работают одинаково)
is_leaf = level.is_leaf_node()
is_root = level.is_root_node()
7.5. Практический пример: Поиск дочерних и потомков¶
См. полную реализацию: src/planiqum/core/hierarchy/libs/direct/record_cleaner.py
Поиск дочерних уровней (прямые дети)¶
# ✅ Правильный legacy подход
all_levels = Level.objects.filter(type=level.type)
child_levels = []
for potential_child in all_levels:
parents = potential_child.get_parents_with_self()
if level in parents and potential_child.id != level.id:
child_levels.append(potential_child)
Почему перебор:
- M2M parents не синхронизируется с FK child автоматически
- MPTT методы инвертированы
- related_name от FK работает наоборот
Поиск всех потомков (прямые и непрямые)¶
# ✅ Правильный legacy подход
all_levels = Level.objects.filter(type=level.type)
all_descendant_levels = []
for potential_descendant in all_levels:
if potential_descendant.id == level.id:
continue
ancestors = potential_descendant.get_all_parents_with_self()
if level in ancestors:
all_descendant_levels.append(potential_descendant)
Пример использования:
Для иерархии segment → brand → product → dpu:
- Поиск детей brand: вернёт [product]
- Поиск потомков brand: вернёт [product, dpu]
8. Примеры¶
8.1. Пример: Получение всех предков¶
# ✅ Legacy подход (рекомендуемый)
ancestors = brand.get_all_parents_with_self()
# Вернёт: [brand, product, dpu]
# ❌ MPTT подход (неправильно без понимания)
descendants = brand.get_descendants()
# Вернёт: [] (т.к. brand - листовой в MPTT!)
# ✅ MPTT с пониманием инверсии
# Legacy логика противоположна MPTT: get_ancestors() в MPTT = потомки в legacy
mptt_ancestors = brand.get_ancestors()
# Вернёт: [dpu, product] - это "дети" в legacy логике!
8.2. Пример: Получение родителей¶
# ✅ Legacy подход (рекомендуемый)
parents = product.get_parents_with_self()
# Вернёт: [product, brand, category]
# ❌ MPTT подход (неправильно без понимания)
children = product.get_children()
# Вернёт: [brand, category] - но это РОДИТЕЛИ в legacy логике!
# ✅ MPTT с пониманием инверсии и комментарием
# Legacy логика противоположна MPTT: get_children() возвращает родителей
mptt_children = list(product.get_children())
# Вернёт: [brand, category] - родители в legacy логике
9. Тесты и шпаргалки¶
См. полные тесты:
- src/planiqum/core/hierarchy/tests/test_mptt_and_legacy_navigation.py - тесты MPTT методов и legacy обёрток
- src/planiqum/core/hierarchy/tests/direct/test_cascade_deletion.py - тесты удаления с legacy подходом (практический пример)
Практические примеры:
- src/planiqum/core/hierarchy/libs/direct/record_cleaner.py - поиск дочерних и потомков через legacy подход
Быстрая справка:
- .cursor/rules/hierarchy.mdc - правила для AI и разработчиков
10. Чек-лист для code review¶
При проверке кода с иерархией убедитесь:
- [ ] Используется legacy подход (
get_parents_with_self(),get_all_parents_with_self()) - [ ] Если используются MPTT методы напрямую - есть явный комментарий о инверсии
- [ ] Поиск дочерних/потомков реализован через перебор + проверку
- [ ] Нет прямых обращений к
Level.objects.filter(parents=level) - [ ] Нет использования
level.mptt_parents.all()для поиска детей - [ ] В тестах есть ASCII-диаграмма иерархии
Важно: описанные настройки и сценарии могут отличаться в вашей инсталляции Planiqum
За уточнениями и методологической поддержкой обращайтесь в компанию
ЮНИК СОФТ