← Назад к вопросам
В Django есть абстрактные модели, полиморфизм, инстансы — как ты проектировал и работал с подобными моделями
2.7 Senior🔥 91 комментариев
#Django#Архитектура и паттерны
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
# Проектирование Django моделей: наследование и полиморфизм
Типы наследования в Django
Django поддерживает три типа наследования моделей:
1. Abstract Base Classes (абстрактные базовые классы)
Используется для повторяющихся полей и методов. Абстрактная модель не создаёт таблицу в БД.
from django.db import models
from django.utils import timezone
class TimeStampedModel(models.Model):
"""Абстрактная базовая модель с полями created_at и updated_at"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Article(TimeStampedModel):
title = models.CharField(max_length=200)
content = models.TextField()
class Meta:
db_table = 'articles'
class Comment(TimeStampedModel):
author = models.CharField(max_length=100)
text = models.TextField()
class Meta:
db_table = 'comments'
# SQL:
# CREATE TABLE articles (id, title, content, created_at, updated_at)
# CREATE TABLE comments (id, author, text, created_at, updated_at)
Когда использовать:
- Общие поля для многих моделей (timestamps, soft delete, uuid)
- Общие методы и свойства
- Не нужно полиморфизм в БД
Минусы:
- Нет полиморфизма на уровне БД
- Нельзя запросить все Article и Comment одним запросом
2. Multi-table Inheritance (наследование с несколькими таблицами)
Каждая модель имеет свою таблицу + родительская таблица. Django автоматически создаёт связь.
class Vehicle(models.Model):
"""Базовая модель для всех транспортных средств"""
name = models.CharField(max_length=100)
speed = models.IntegerField()
class Meta:
db_table = 'vehicles'
class Car(Vehicle):
"""Расширяет Vehicle"""
doors = models.IntegerField()
class Meta:
db_table = 'cars'
class Motorcycle(Vehicle):
"""Расширяет Vehicle"""
has_sidecar = models.BooleanField()
class Meta:
db_table = 'motorcycles'
# SQL:
# CREATE TABLE vehicles (id, name, speed)
# CREATE TABLE cars (vehicle_ptr_id, doors) -- vehicle_ptr_id это внешний ключ на vehicles
# CREATE TABLE motorcycles (vehicle_ptr_id, has_sidecar)
# Использование:
car = Car.objects.create(name="Toyota", speed=200, doors=4)
moto = Motorcycle.objects.create(name="Harley", speed=180, has_sidecar=False)
# Доступ к полям родителя
print(car.name) # "Toyota"
print(car.speed) # 200
print(car.doors) # 4
# Запросы
all_vehicles = Vehicle.objects.all() # только базовые поля
cars = Car.objects.all() # все поля Car + Vehicle
Когда использовать:
- Полиморфизм: хочешь запросить все подклассы одного базового класса
- Разные наборы полей для каждого типа
- Сложная бизнес-логика, специфичная для каждого типа
Минусы:
- Дополнительные JOIN'ы в каждом запросе
- Может быть медленным с большим числом подклассов
- Сложнее работать с миграциями
3. Proxy Models (прокси модели)
Используют ту же таблицу, но позволяют переопределить поведение и менеджеры.
class Person(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
role = models.CharField(
max_length=20,
choices=[('teacher', 'Teacher'), ('student', 'Student')]
)
class Meta:
db_table = 'persons'
# Прокси модели — используют ту же таблицу
class Teacher(Person):
class Meta:
proxy = True
objects = models.Manager.from_queryset(TeacherQuerySet)()
def give_grade(self, student, grade):
"""Метод специфичный для учителей"""
student.grade = grade
student.save()
class Student(Person):
class Meta:
proxy = True
objects = models.Manager.from_queryset(StudentQuerySet)()
def get_grades(self):
"""Метод специфичный для студентов"""
return self.grades.all()
# SQL:
# CREATE TABLE persons (id, first_name, last_name, role) -- одна таблица!
# Использование:
teacher = Person.objects.create(first_name="John", last_name="Doe", role="teacher")
teacher_obj = Teacher.objects.get(pk=teacher.id)
teacher_obj.give_grade(student, "A")
student_list = Student.objects.all() # фильтрует по role="student"
Когда использовать:
- Разные менеджеры для одного типа данных
- Разное поведение (методы) для одной таблицы
- Фильтрация по типу без отдельной таблицы
Практический пример: система контента
from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.utils import timezone
# Абстрактная база
class PublishableModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
is_published = models.BooleanField(default=False)
class Meta:
abstract = True
def publish(self):
self.is_published = True
self.published_at = timezone.now()
self.save()
def unpublish(self):
self.is_published = False
self.published_at = None
self.save()
# Базовая модель для полиморфизма
class Content(PublishableModel):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
author = models.ForeignKey('User', on_delete=models.CASCADE)
class Meta:
db_table = 'content'
def __str__(self):
return self.title
# Подклассы с разными полями
class Article(Content):
content = models.TextField()
featured_image = models.ImageField(upload_to='articles/')
category = models.CharField(max_length=100)
class Meta:
db_table = 'articles'
class VideoPost(Content):
video_url = models.URLField()
duration = models.IntegerField() # в секундах
thumbnail = models.ImageField(upload_to='videos/')
class Meta:
db_table = 'video_posts'
class Gallery(Content):
description = models.TextField()
class Meta:
db_table = 'galleries'
class GalleryImage(models.Model):
gallery = models.ForeignKey(Gallery, on_delete=models.CASCADE, related_name='images')
image = models.ImageField(upload_to='gallery_images/')
caption = models.CharField(max_length=200, blank=True)
order = models.IntegerField(default=0)
class Meta:
ordering = ['order']
db_table = 'gallery_images'
# Использование
article = Article.objects.create(
title="Python Best Practices",
slug="python-best-practices",
author=author,
content="...",
featured_image="...",
category="Programming"
)
article.publish()
# Запросить все опубликованные статьи
published_articles = Article.objects.filter(is_published=True)
# Запросить все опубликованные контенты (полиморфизм)
all_content = Content.objects.filter(is_published=True)
for item in all_content:
if isinstance(item, Article):
print(f"Article: {item.title}")
elif isinstance(item, VideoPost):
print(f"Video: {item.title}")
elif isinstance(item, Gallery):
print(f"Gallery: {item.title}")
Пример: система уведомлений
class Notification(models.Model):
"""Базовая модель"""
user = models.ForeignKey('User', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
is_read = models.BooleanField(default=False)
class Meta:
db_table = 'notifications'
class EmailNotification(Notification):
subject = models.CharField(max_length=200)
body = models.TextField()
class Meta:
db_table = 'email_notifications'
def send(self):
send_mail(self.subject, self.body, [self.user.email])
class PushNotification(Notification):
title = models.CharField(max_length=100)
message = models.CharField(max_length=300)
icon_url = models.URLField()
class Meta:
db_table = 'push_notifications'
def send(self):
send_push_to_device(self.user.device_token, self.title, self.message)
class SMSNotification(Notification):
phone = models.CharField(max_length=20)
message = models.CharField(max_length=160)
class Meta:
db_table = 'sms_notifications'
def send(self):
send_sms(self.phone, self.message)
# Использование
for notification in Notification.objects.filter(is_read=False):
notification.send() # полиморфизм! вызовется нужный send()
notification.is_read = True
notification.save()
Советы по проектированию
1. Abstract vs Multi-table
# ✅ Используй Abstract, если:
# - Не нужна фильтрация по типу
# - Чистый способ переиспользовать поля
class TimeStamped(models.Model):
created = models.DateTimeField(auto_now_add=True)
class Meta:
abstract = True
# ✅ Используй Multi-table, если:
# - Нужна фильтрация: Content.objects.filter(is_published=True)
# - Разные поля для разных типов
class Content(models.Model):
title = models.CharField(max_length=200)
class Meta:
db_table = 'content'
2. Avoid the Diamond Problem
# ❌ Проблема: множественное наследование в Django
class A(models.Model):
field_a = models.CharField(max_length=100)
class Meta:
abstract = True
class B(models.Model):
field_b = models.CharField(max_length=100)
class Meta:
abstract = True
class C(A, B):
field_c = models.CharField(max_length=100)
# Проблема: неясна MRO (Method Resolution Order)
# ✅ Решение: используй одну базовую классов
class BaseFields(models.Model):
field_a = models.CharField(max_length=100)
field_b = models.CharField(max_length=100)
class Meta:
abstract = True
class C(BaseFields):
field_c = models.CharField(max_length=100)
3. Избегай N+1 queries
# ❌ Проблема
contents = Content.objects.all()
for content in contents:
if isinstance(content, Article):
print(content.featured_image) # дополнительный запрос для каждого!
# ✅ Решение: используй select_related
contents = Content.objects.select_related(
'article', 'videopost', 'gallery'
).all()
# Или используй prefetch_related для обратных связей
from django.db.models import Prefetch
contents = Gallery.objects.prefetch_related(
Prefetch('images', queryset=GalleryImage.objects.order_by('order'))
).all()
4. Правильные индексы
class Article(Content):
content = models.TextField()
is_featured = models.BooleanField(default=False, db_index=True)
views_count = models.IntegerField(default=0)
class Meta:
db_table = 'articles'
indexes = [
models.Index(fields=['is_published', 'published_at'], name='published_idx'),
models.Index(fields=['author', '-created_at'], name='author_date_idx'),
]
Антипаттерны
# ❌ Слишком глубокое наследование
class A(models.Model): pass
class B(A): pass
class C(B): pass
class D(C): pass
# 4 таблицы, 4 JOIN'а — медленно!
# ❌ Миксование Abstract и Multi-table
class Base(models.Model):
class Meta:
abstract = True
class Middle(Base, models.Model): # ошибка!
class Meta:
db_table = 'middle'
# ❌ Игнорирование migrations
# Django добавил новое поле в абстрактный класс?
# Нужна миграция для всех наследников!
Итоги
| Тип | Таблица | JOIN'ы | Полиморфизм | Когда использовать |
|---|---|---|---|---|
| Abstract | Нет | 0 | Нет | Переиспользование полей |
| Multi-table | Да, несколько | Да | Да | Разные типы с полиморфизмом |
| Proxy | Одна | Нет | Да (с isinstance) | Разное поведение, один набор полей |
Частая ошибка: использовать Multi-table когда достаточно Abstract, или наоборот. Выбирай в зависимости от:
- Нужен ли полиморфизм запросов?
- Есть ли разные поля?
- Какова производительность?