Создание древовидной модели категорий
Иерархические данные
Категории сайта статей вложены друг в друга:
Программирование
├── Python
│ ├── Django
│ └── FastAPI
└── JavaScript
└── React
Дизайн
└── UI/UX
Самый простой способ — ForeignKey на самого себя (parent). Но тогда, чтобы получить все статьи раздела «Программирование» со всеми подразделами, придётся делать рекурсивные запросы — по одному на уровень.
django-mptt реализует алгоритм Modified Preorder Tree Traversal (вложенные множества): у каждого узла хранятся числа lft, rght, level, tree_id. По ним любые потомки или предки выбираются одним запросом: «все узлы с lft между lft и rght родителя».
Установка
pip install django-mptt
INSTALLED_APPS = [
...
"mptt",
"modules.blog.apps.BlogConfig",
]
Модель категории
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
title = models.CharField(max_length=255, verbose_name="Название категории")
slug = models.SlugField(max_length=255, verbose_name="URL категории", blank=True)
description = models.TextField(verbose_name="Описание категории", max_length=300, blank=True)
parent = TreeForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
db_index=True,
related_name="children",
verbose_name="Родительская категория",
)
class MPTTMeta:
order_insertion_by = ["title"]
class Meta:
verbose_name = "Категория"
verbose_name_plural = "Категории"
db_table = "app_categories"
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse("articles_by_category", kwargs={"slug": self.slug})
MPTTModelдобавит служебные поляlft,rght,tree_id,levelи поддерживает их при изменениях.TreeForeignKey— какForeignKey, но в формах показывает дерево с отступами.order_insertion_by— дочерние узлы сортируются по названию.
Связь статей с категориями
class Article(models.Model):
...
category = TreeForeignKey("Category", on_delete=models.PROTECT, related_name="articles", verbose_name="Категория")
PROTECT не даст случайно удалить категорию вместе со всеми статьями.
python manage.py makemigrations
python manage.py migrate
Если в базе уже есть статьи, Django спросит значение по умолчанию для нового обязательного поля — создайте сначала категорию и укажите её id, либо временно сделайте поле null=True.
Админка с деревом
from mptt.admin import DraggableMPTTAdmin
from .models import Article, Category
@admin.register(Category)
class CategoryAdmin(DraggableMPTTAdmin):
list_display = ("tree_actions", "indented_title", "id", "title", "slug")
list_display_links = ("title", "slug")
prepopulated_fields = {"slug": ("title",)}
DraggableMPTTAdmin показывает категории деревом и позволяет перетаскивать их мышью.
Работа с деревом
>>> python = Category.objects.get(slug="python")
>>> python.get_ancestors() # [Программирование]
>>> python.get_children() # [Django, FastAPI]
>>> python.get_descendants(include_self=True)
>>> python.is_leaf_node() # False
>>> python.level # 1
# все статьи раздела вместе с подразделами — один запрос
>>> Article.objects.filter(category__in=python.get_descendants(include_self=True))
>>> Category.objects.root_nodes() # корневые категории
Итог
- Деревья в реляционной БД эффективно хранит django-mptt (вложенные множества).
Category(MPTTModel)+parent = TreeForeignKey("self", ...),MPTTMeta.order_insertion_by.- Статья ссылается на категорию через
TreeForeignKeyсPROTECT. - Методы
get_ancestors,get_children,get_descendants— без рекурсии и лишних запросов.
Практика решено 0 из 5
Какую задачу решает django-mptt?
Опишите модель Category на основе MPTTModel: поля title (CharField 255), slug (SlugField 255),
parent — TreeForeignKey("self", ...) с on_delete=models.CASCADE, null=True, blank=True, related_name="children".
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
title = models.CharField(max_length=255, verbose_name="Название категории")
slug = models.SlugField(max_length=255, verbose_name="URL категории", blank=True)
parent = TreeForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
db_index=True,
related_name="children",
verbose_name="Родительская категория",
)
Добавьте в модель Category вложенный класс MPTTMeta с сортировкой при вставке по полю title.
class Category(MPTTModel):
title = models.CharField(max_length=255)
parent = TreeForeignKey("self", on_delete=models.CASCADE, null=True, blank=True, related_name="children")
class MPTTMeta:
order_insertion_by = ["title"]
Какие методы есть у узла дерева django-mptt?
Добавьте в модель Article связь с категорией: category = TreeForeignKey("Category", ...) с on_delete=models.PROTECT,
related_name="articles".
class Article(models.Model):
title = models.CharField(max_length=255)
category = TreeForeignKey("Category", on_delete=models.PROTECT, related_name="articles", verbose_name="Категория")
Войдите, чтобы отмечать пройденные уроки.