← Назад к вопросам

В 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, или наоборот. Выбирай в зависимости от:

  • Нужен ли полиморфизм запросов?
  • Есть ли разные поля?
  • Какова производительность?