Meta и менеджеры в Django ORM | Курс Django ORM урок 1.3
Цель урока
Разобраться с классом Meta внутри модели, что он контролирует на уровне БД и на уровне Django. Понять, что такое менеджер модели, как работает objects по умолчанию и зачем вообще нужен этот слой абстракции.
Необходимые знания
- Уроки 1.1 и 1.2: поля и связи
- Базовое понимание индексов в SQL
Класс Meta в Django ORM
Meta, внутренний класс модели, который управляет поведением модели в целом, а не отдельных полей. Он не создает колонок, он влияет на таблицу, индексы, ограничения и поведение ORM.
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["name"]
verbose_name = "product"
verbose_name_plural = "products"
db_table = "shop_product"
Разберем каждую опцию.
ordering - сортировка по умолчанию
class Meta:
ordering = ["name"] # по возрастанию
ordering = ["-created_at"] # по убыванию (минус = DESC)
ordering = ["-created_at", "name"] # несколько полей
Когда задан ordering, каждый QuerySet без явного order_by() автоматически получает ORDER BY:
Product.objects.all()
SELECT * FROM shop_product ORDER BY name ASC;
Это удобно, но есть скрытая цена. ordering применяется к каждому запросу, включая те, где сортировка не нужна: count(), exists(), агрегации. До Django 3.0 это вызывало лишние JOIN при ordering по связанным полям. Сейчас Django умнее, но привычка осознанно управлять сортировкой указывая явный order_by(), правильная.
Сортировка по связанному полю:
class Meta:
ordering = ["category__name", "name"]
Генерирует JOIN при каждом запросе без явного order_by(), это потенциально дорого на больших таблицах.
Чтобы получить QuerySet без сортировки по умолчанию:
Product.objects.order_by() # сброс ordering, пустой list убирает все
verbose_name и verbose_name_plural
class Meta:
verbose_name = "товар"
verbose_name_plural = "товары"
Используется в Django Admin и формах. На SQL не влияет. Если не задать, Django генерирует из имени класса: Product → "product" / "products".
db_table - имя таблицы
class Meta:
db_table = "products" # вместо shop_product
По умолчанию Django формирует имя как appname_modelname. Менять стоит только если:
- Работаете с legacy БД, где таблицы уже названы иначе
- Нужно соответствие определенному соглашению об именах
При смене db_table на существующей БД нужна миграция, которая переименует таблицу.
indexes - составные и специализированные индексы
Для одиночных индексов на поле достаточно db_index=True. Meta.indexes нужен для:
- составных индексов по нескольким полям
- частичных индексов по условию
- индексов с особыми параметрами
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=200)
category = models.ForeignKey(Category, on_delete=models.PROTECT)
is_active = models.BooleanField(default=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
# Составной индекс ускоряет запросы с фильтром по обоим полям
models.Index(fields=["category", "is_active"], name="product_category_active_idx"),
# Сортировка по убыванию для запросов ORDER BY created_at DESC
models.Index(fields=["-created_at"], name="product_created_desc_idx"),
]
SQL, который создает миграция:
CREATE INDEX product_category_active_idx
ON shop_product (category_id, is_active);
CREATE INDEX product_created_desc_idx
ON shop_product (created_at DESC);
Частичный индекс (PostgreSQL)
Индекс только по строкам, где is_active = True. Если активных товаров 10% от общего числа, индекс будет в 10 раз быстрее:
from django.db.models import Q
class Meta:
indexes = [
models.Index(
fields=["price"],
condition=Q(is_active=True),
name="product_active_price_idx",
),
]
CREATE INDEX product_active_price_idx
ON shop_product (price)
WHERE is_active = true;
Этот индекс используется только если запрос включает условие is_active = True. Подробно про индексы в уроке 5.1.
constraints - ограничения на уровне БД
from django.db.models import CheckConstraint, UniqueConstraint, Q
class Review(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.PositiveSmallIntegerField()
class Meta:
constraints = [
# Один пользователь, один отзыв на продукт
UniqueConstraint(
fields=["product", "user"],
name="unique_review_per_user",
),
# Рейтинг только от 1 до 5
CheckConstraint(
condition=Q(rating__gte=1) & Q(rating__lte=5),
name="valid_rating_range",
),
]
SQL:
ALTER TABLE shop_review
ADD CONSTRAINT unique_review_per_user UNIQUE (product_id, user_id);
ALTER TABLE shop_review
ADD CONSTRAINT valid_rating_range CHECK (rating >= 1 AND rating <= 5);
CheckConstraint, ограничение проверяется базой данных при каждой INSERT и UPDATE. Даже если обойти валидацию Django, БД откажет в записи невалидных данных.
Разница между unique_together (устаревший с Django 4.2, рекомендуется UniqueConstraint) и UniqueConstraint:
# Старый способ, устаревший с Django 4.2, но еще работает
class Meta:
unique_together = [("product", "user")]
# Новый способ, больше возможностей
class Meta:
constraints = [
UniqueConstraint(fields=["product", "user"], name="unique_review_per_user"),
]
UniqueConstraint поддерживает дополнительные параметры: condition для частичного unique constraint, include для covering index, nulls_distinct (Django 5.0+, PostgreSQL 15+). Подробно, в уроке 6.3.
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)
# автоматически получает created_at и updated_at
Product создаст одну таблицу со всеми полями, включая унаследованные. Это просто переиспользование кода, никакого наследования на уровне БД. Подробно о наследовании моделей в уроке 9.1.
Менеджеры моделей
Менеджер это объект, через который Django ORM взаимодействует с базой данных. Каждая модель получает менеджер по умолчанию с именем objects.
# Все эти вызовы идут через менеджер objects
Product.objects.all()
Product.objects.filter(is_active=True)
Product.objects.get(id=1)
Product.objects.create(name="iPhone", price=999)
Что такое менеджер
Менеджер это экземпляр класса Manager. Он привязан к модели (знает, с какой таблицей работать) и предоставляет методы для построения QuerySet.
from django.db import models
# Manager - это класс
print(type(Product.objects)) # <class 'django.db.models.manager.Manager'>
# objects - атрибут класса Product, экземпляр Manager
# Через него все запросы к таблице shop_product
Когда вы пишете Product.objects.filter(...), происходит следующее:
Product.objects, обращение к менеджеру.filter(...), менеджер создает QuerySet- QuerySet знает модель и строит SQL
- SQL уходит в PostgreSQL только при итерации (ленивость)
Зачем нужен менеджер как отдельный слой
Менеджер отделяет "получение данных" от "данных как таковых". Модель описывает структуру, менеджер, способы доступа к данным.
Это важно, когда нужно добавить логику на уровне запросов, общую для всего приложения. Например, все запросы к Product должны возвращать только активные товары. Без кастомного менеджера каждый запрос в коде должен иметь filter(is_active=True). С менеджером, достаточно одного места.
Несколько менеджеров
Модель может иметь несколько менеджеров:
class Product(models.Model):
name = models.CharField(max_length=200)
is_active = models.BooleanField(default=True)
# Стандартный менеджер, все продукты
objects = models.Manager()
# Второй менеджер, только активные
active = models.Manager() # пока не переопределен, то же самое что objects
Кастомные менеджеры, которые фильтруют данные, мы создадим в уроке 8. Сейчас важно понять: objects, это не встроенная часть ORM, это менеджер по умолчанию. Его можно переопределить или добавить рядом другой.
Первый менеджер - менеджер по умолчанию
Django использует первый объявленный менеджер как менеджер по умолчанию. Это влияет на:
- Django Admin, использует менеджер по умолчанию для выборки объектов
related objectsпри доступе через обратные связи
class Product(models.Model):
# Если первый менеджер фильтрует данные - это может сломать Admin
active = models.Manager() # первый = по умолчанию
objects = models.Manager()
# Правильно: objects первым, чтобы админка видела все объекты
objects = models.Manager()
active = models.Manager()
Лучше явно указать use_in_migrations = True и Meta.default_manager_name если меняете порядок. Но самый безопасный вариант, всегда оставлять objects первым менеджером.
Что получилось в нашем проекте
Посмотрим на полные Meta для наших моделей с учетом всего изученного:
class Product(models.Model):
# ... поля ...
class Meta:
ordering = ["name"]
verbose_name = "product"
verbose_name_plural = "products"
indexes = [
models.Index(fields=["category", "is_active"], name="product_category_active_idx"),
models.Index(fields=["-created_at"], name="product_created_desc_idx"),
]
class Review(models.Model):
# ... поля ...
class Meta:
ordering = ["-created_at"]
constraints = [
models.UniqueConstraint(
fields=["product", "user"],
name="unique_review_per_user",
),
models.CheckConstraint(
condition=models.Q(rating__gte=1) & models.Q(rating__lte=5),
name="valid_rating_range",
),
]
class Category(models.Model):
# ... поля ...
class Meta:
verbose_name_plural = "categories"
ordering = ["name"]
Практическое задание
-
Добавьте в модель
Orderconstraint, который запрещает отрицательныйtotal_price. Сделайте миграцию. Проверьте через psql (\d shop_order), что constraint появился. -
Добавьте в модель
Productсоставной индекс по полямis_activeиprice. Как называется этот индекс в PostgreSQL после миграции? -
Создайте абстрактную модель
TimestampedModelс полямиcreated_atиupdated_at. Унаследуй от неёProductиOrder. Убедитесь что миграция не создает отдельную таблицуTimestampedModel. -
Выполните в
shell:python from shop.models import Product print(type(Product.objects)) print(Product.objects.model)Что возвращает каждая из этих строк?
Возможные ошибки
Сортировка по связанному полю в Meta.ordering без индекса
class Meta:
ordering = ["category__name"] # JOIN при каждом запросе
Если запросов много и таблица большая, это заметно влияет на производительность. Лучше явно указывать order_by() там, где нужна сортировка.
Дублирование unique_together и UniqueConstraint
class Meta:
unique_together = [("product", "user")] # устаревший способ
constraints = [
UniqueConstraint(fields=["product", "user"], name="unique_review") # создаст второй constraint
]
Используйте что-то одно. Предпочитай UniqueConstraint, он гибче.
Именование constraints и indexes
Django не позволяет два объекта с одинаковым именем. Названия должны быть уникальными в рамках БД. Принятое соглашение: {table}_{поля}_{тип}, например shop_review_product_user_uniq.
Забыть abstract = True в базовом классе
class TimestampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
# Забыли abstract = True - Django создаст таблицу timestampedmodel
# и будет использовать multi-table inheritance вместо abstract
Связь со следующим уроком
В уроке 1.4 разбираем миграции, что происходит внутри файла миграции, как Django отслеживает изменения схемы, как откатывать и как писать data migrations. После этого урока блок 1 будет закрыт и мы перейдем к работе с данными.
Подписывайтесь на мой Telegram канал
Если вам нужен ментор и вы хотите научиться веб-разработке, узнать подробнее
Авторизуйтесь, чтобы оставить комментарий.
Нет комментариев.
Тут может быть ваша реклама
Пишите info@aisferaic.ru