Django ORM

Наследование моделей | Курс Django ORM урок 9.1

Наследование моделей | Курс Django ORM урок 9.1
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 5

Цель урока

Разобрать три типа наследования моделей Django: Abstract, Multi-table и Proxy. Понять когда каждый применять, что происходит на уровне базы данных и как выбор влияет на запросы.

Необходимые знания


Три типа наследования

Django поддерживает три подхода к наследованию моделей:

Тип Таблица в БД Когда использовать
Abstract Нет отдельной таблицы Переиспользование полей и методов
Multi-table Отдельная таблица + JOIN Полиморфизм, разные типы одной сущности
Proxy Та же таблица Разное поведение для одних данных

Abstract - абстрактная модель

Абстрактная модель не создает таблицу. Её поля и методы наследуются дочерними моделями, каждая из которых создает свою таблицу со всеми полями.

class TimestampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True  # не создает таблицу


class Product(TimestampedModel):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)


class Order(TimestampedModel):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    status = models.CharField(max_length=20)


-- Только эти таблицы, нет таблицы для TimestampedModel
CREATE TABLE shop_product (
    id BIGSERIAL PRIMARY KEY,
    name VARCHAR(200),
    price DECIMAL(10, 2),
    created_at TIMESTAMPTZ,  -- унаследовано
    updated_at TIMESTAMPTZ   -- унаследовано
);

CREATE TABLE shop_order (
    id BIGSERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES auth_user(id),
    status VARCHAR(20),
    created_at TIMESTAMPTZ,  -- унаследовано
    updated_at TIMESTAMPTZ   -- унаследовано
);

Meta в абстрактных моделях

class TimestampedModel(models.Model):
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True
        ordering = ["-created_at"]  # наследуется дочерними

ordering из абстрактного родителя наследуется. Дочерняя модель может переопределить:

class Product(TimestampedModel):
    name = models.CharField(max_length=200)

    class Meta(TimestampedModel.Meta):  # явное наследование Meta
        ordering = ["name"]  # переопределить

Переиспользование методов

class SoftDeleteModel(models.Model):
    is_deleted = models.BooleanField(default=False)
    deleted_at = models.DateTimeField(null=True)

    def delete(self, *args, **kwargs):
        self.is_deleted = True
        self.deleted_at = timezone.now()
        self.save()

    class Meta:
        abstract = True


class Product(SoftDeleteModel):
    name = models.CharField(max_length=200)
    # Автоматически получает delete(), is_deleted, deleted_at

Когда использовать Abstract

  • Общие поля для нескольких моделей (created_at, updated_at, is_deleted)
  • Общие методы без полиморфизма
  • Когда не нужен QuerySet по всем дочерним моделям сразу

Multi-table inheritance - наследование с JOIN

Зачем нужен Multi-table

Представьте каталог транспортных средств: легковые автомобили, грузовики, мотоциклы. У каждого типа свои поля, у легкового doors и trunk_volume, у грузовика payload_capacity. И общие поля: make, model, year, price.

Можно сделать одну большую таблицу со всеми полями, где большинство будет NULL в зависимости от типа. Можно сделать полностью отдельные модели без связи. Но тогда нельзя одним запросом получить "все транспортные средства дешевле 40 000".

Multi-table решает именно это: общий QuerySet через родительскую модель и специфичные поля в дочерних таблицах.

# Можно запрашивать через родителя, получите и Car, и Truck
Vehicle.objects.filter(price__lt=40000).order_by("year")

# И через дочернюю, только легковые со своими полями
Car.objects.filter(doors=4)

Обратная сторона это JOIN при каждом запросе к дочерней модели. Это отличает Multi-table от Abstract. Abstract не позволяет делать запросы через родителя, зато без JOIN.

Каждая модель в иерархии создает свою таблицу. Дочерняя таблица связана с родительской через OneToOne:

class Vehicle(models.Model):
    make = models.CharField(max_length=100)
    model = models.CharField(max_length=100)
    year = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)


class Car(Vehicle):
    doors = models.IntegerField()
    trunk_volume = models.FloatField()


class Truck(Vehicle):
    payload_capacity = models.FloatField()
    has_trailer = models.BooleanField(default=False)


-- Таблица родителя
CREATE TABLE shop_vehicle (
    id BIGSERIAL PRIMARY KEY,
    make VARCHAR(100),
    model VARCHAR(100),
    year INTEGER,
    price DECIMAL(10, 2)
);

-- Таблица дочерней модели
CREATE TABLE shop_car (
    vehicle_ptr_id BIGINT PRIMARY KEY REFERENCES shop_vehicle(id),
    doors INTEGER,
    trunk_volume FLOAT
);

CREATE TABLE shop_truck (
    vehicle_ptr_id BIGINT PRIMARY KEY REFERENCES shop_vehicle(id),
    payload_capacity FLOAT,
    has_trailer BOOLEAN
);

Дочерняя таблица не имеет собственного id, используется vehicle_ptr_id как PK и FK.

Запросы через Multi-table

# Создание
car = Car.objects.create(
    make="Toyota", model="Camry", year=2023, price=Decimal("35000"),
    doors=4, trunk_volume=508.0
)
-- Два INSERT: сначала родитель, потом дочерний
INSERT INTO shop_vehicle (make, model, year, price) VALUES ('Toyota', 'Camry', 2023, 35000);
INSERT INTO shop_car (vehicle_ptr_id, doors, trunk_volume) VALUES (1, 4, 508.0);


# Запрос по дочерней модели
Car.objects.filter(doors=4)
-- JOIN родительской таблицы
SELECT shop_vehicle.*, shop_car.doors, shop_car.trunk_volume
FROM shop_car
INNER JOIN shop_vehicle ON shop_car.vehicle_ptr_id = shop_vehicle.id
WHERE shop_car.doors = 4;


# Запрос по родительской модели, возвращает Vehicle объекты
Vehicle.objects.filter(price__lt=40000)
-- Только shop_vehicle, без JOIN
SELECT * FROM shop_vehicle WHERE price < 40000;

Объект Vehicle не знает является ли он Car или Truck. Для доступа к дочернему:

vehicle = Vehicle.objects.get(id=1)
try:
    car = vehicle.car  # OneToOne доступ
except Car.DoesNotExist:
    pass  # это не Car


SELECT * FROM shop_car WHERE vehicle_ptr_id = 1;

Производительность Multi-table

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

Multi-table наследование редко используется в приложениях Django из-за сложности и JOIN. Чаще используют Abstract + отдельные модели или Generic FK.


Proxy models - другое поведение, та же таблица

Зачем нужны Proxy модели

Представьте, что у вас есть модель Order и менеджер (человек), работающий с новыми заказами. Ему нужна кнопка "подтвердить", специфичные фильтры, своя сортировка. У менеджера склада, другие нужды, он видит только доставленные заказы и работает с ними иначе.

Один из способов, добавлять методы и менеджеры (не человек) в саму модель Order. Но она начинает разрастаться и содержать логику для разных ролей сразу.

Proxy модели решают эту проблему, разные классы для одной таблицы, каждый со своим менеджером и методами.

# Без Proxy - всё свалено в одну модель
order = Order.objects.filter(status="pending").first()
order.status = "processing"  # логика разбросана по коду
order.save()

# С Proxy - бизнес-логика инкапсулирована
order = PendingOrder.objects.first()
order.approve()  # метод находится там, где нужен

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

Proxy модель не добавляет полей, это другой класс для той же таблицы БД:

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    status = models.CharField(max_length=20)
    total_price = models.DecimalField(max_digits=10, decimal_places=2)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-created_at"]


class PendingOrderManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status="pending")


class PendingOrder(Order):
    """Proxy для работы только с отложенными заказами."""

    objects = PendingOrderManager()

    class Meta:
        proxy = True
        ordering = ["created_at"]  # другая сортировка

    def approve(self):
        self.status = "processing"
        self.save(update_fields=["status"])


class DeliveredOrderManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(status="delivered")


class DeliveredOrder(Order):
    """Proxy для работы с доставленными заказами."""

    objects = DeliveredOrderManager()

    class Meta:
        proxy = True


-- Нет новых таблиц, всё в shop_order
-- PendingOrder.objects.all() генерирует:
SELECT * FROM shop_order WHERE status = 'pending' ORDER BY created_at;

Использование Proxy

# PendingOrder и Order это разные классы для той же таблицы
pending = PendingOrder.objects.all()   # WHERE status = 'pending'
all_orders = Order.objects.all()       # все заказы

# Одна строка в БД, два класса для работы с ней
order = Order.objects.get(id=1)
pending_order = PendingOrder.objects.get(id=1)
# id одинаковый, данные из одной строки

Proxy для Django Admin

Proxy часто используют чтобы по-разному показать один и тот же набор данных в Admin:

# admin.py
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    list_display = ["id", "user", "status", "total_price"]

@admin.register(PendingOrder)
class PendingOrderAdmin(admin.ModelAdmin):
    list_display = ["id", "user", "created_at"]
    actions = ["approve_orders"]
    # Свой интерфейс для менеджеров, работающих с новыми заказами

Сравнение типов наследования

Abstract - поля копируются в каждую таблицу

class TimestampedModel(models.Model):
    created_at = ...
    class Meta:
        abstract = True

# Product и Order получают свои колонки created_at
# Нет таблицы TimestampedModel
# Нет JOIN при запросах

Multi-table - поля в разных таблицах, JOIN

class Vehicle(models.Model):
    make = ...

class Car(Vehicle):
    doors = ...
# Таблица shop_vehicle + shop_car
# JOIN при каждом запросе к Car

Proxy - нет новых полей, нет новых таблиц

class PendingOrder(Order):
    class Meta:
        proxy = True
# Таблица та же - shop_order
# Нет JOIN, другой класс Python

Практическое задание

  1. Создайте абстрактную модель TimestampedModel с created_at и updated_at. Примените её к Product, Order, Review. Посмотрите SQL через sqlmigrate, убедитесь что таблицы имеют нужные колонки.

  2. Создайте Proxy модель FeaturedProduct(Product) с кастомным менеджером который фильтрует по is_featured=True. Добавьте метод feature() который устанавливает is_featured=True. Проверьте что FeaturedProduct.objects.all() и Product.objects.all() работают с одной таблицей.

  3. Создайте Proxy модели PendingOrder, ProcessingOrder, DeliveredOrder, каждая с менеджером для своего статуса. Зарегистрируйте их в Django Admin.

  4. Реализуйте Multi-table наследование: базовая модель Notification (user, created_at, is_read) и дочерние OrderNotification (order) и ReviewNotification (review). Напишите запрос который получает все OrderNotification с JOIN на Notification через select_related.

  5. Сравните SQL для OrderNotification.objects.all() и Notification.objects.all(). Чем отличается? Почему Multi-table наследование может быть медленнее Abstract?


Возможные ошибки

Multi-table вместо Abstract когда не нужен полиморфизм

# Плохо когда JOIN на каждый запрос
class ProductBase(models.Model):
    name = ...
    created_at = ...

class Product(ProductBase):
    price = ...
    # Каждый Product запрос делает JOIN

# Правильно использовать Abstract если не нужен QuerySet по ProductBase
class ProductBase(models.Model):
    name = ...
    created_at = ...
    class Meta:
        abstract = True

Proxy с новыми полями

# FieldError - Proxy не может добавлять новые поля
class PremiumProduct(Product):
    premium_features = models.TextField()  # нельзя!
    class Meta:
        proxy = True

Если нужны новые поля, Multi-table или отдельная модель с OneToOne.

Abstract с related_name

class Likeable(models.Model):
    likes = models.ManyToManyField(User, related_name="liked")  # конфликт!
    class Meta:
        abstract = True

class Post(Likeable): ...
class Comment(Likeable): ...
# SystemCheckError: reverse accessor clash

При abstract с M2M или FK, используйте %(class)s в related_name:

likes = models.ManyToManyField(User, related_name="%(class)s_liked")
# Post: related_name="post_liked"
# Comment: related_name="comment_liked"

Связь со следующим уроком

В уроке 9.2 разберем продвинутые паттерны: историческое логирование изменений, денормализация данных через computed fields и паттерн агрегации статистики без повторных вычислений.


<< Урок 8.1

Урок 9.2 >>


Подписывайтесь на мой Telegram канал

Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее

Авторизуйтесь, чтобы оставить комментарий.

Комментариев: 0

Нет комментариев.

Тут может быть ваша реклама

Пишите info@aisferaic.ru

Похожие туториалы