Наследование моделей | Курс Django ORM урок 9.1
Цель урока
Разобрать три типа наследования моделей 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
Практическое задание
-
Создайте абстрактную модель
TimestampedModelсcreated_atиupdated_at. Примените её кProduct,Order,Review. Посмотрите SQL черезsqlmigrate, убедитесь что таблицы имеют нужные колонки. -
Создайте Proxy модель
FeaturedProduct(Product)с кастомным менеджером который фильтрует поis_featured=True. Добавьте методfeature()который устанавливаетis_featured=True. Проверьте чтоFeaturedProduct.objects.all()иProduct.objects.all()работают с одной таблицей. -
Создайте Proxy модели
PendingOrder,ProcessingOrder,DeliveredOrder, каждая с менеджером для своего статуса. Зарегистрируйте их в Django Admin. -
Реализуйте Multi-table наследование: базовая модель
Notification(user, created_at, is_read) и дочерниеOrderNotification(order) иReviewNotification(review). Напишите запрос который получает всеOrderNotificationс JOIN наNotificationчерезselect_related. -
Сравните 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 и паттерн агрегации статистики без повторных вычислений.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru