Создание древовидной модели категорий

⏱ 25 мин · 🧩 заданий: 5

Иерархические данные

Категории сайта статей вложены друг в друга:

Программирование
├── 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

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Какую задачу решает django-mptt?

Задание 2 Написать код

Опишите модель Category на основе MPTTModel: поля title (CharField 255), slug (SlugField 255), parent — TreeForeignKey("self", ...) с on_delete=models.CASCADE, null=True, blank=True, related_name="children".

Задание 3 Написать код

Добавьте в модель Category вложенный класс MPTTMeta с сортировкой при вставке по полю title.

Задание 4 Несколько вариантов

Какие методы есть у узла дерева django-mptt?

Задание 5 Написать код

Добавьте в модель Article связь с категорией: category = TreeForeignKey("Category", ...) с on_delete=models.PROTECT, related_name="articles".

Войдите, чтобы отмечать пройденные уроки.