Django ORM

Связи между моделями Django | Курс Django ORM урок 1.2

Связи между моделями Django | Курс Django ORM урок 1.2
Mikhail
Автор
Mikhail
Опубликовано 16.03.2026
0,0
Views 14

Цель урока

Разобраться, как Django ORM реализует три типа связей между моделями, что именно создается в PostgreSQL для каждого из них, и как параметры on_delete, related_name и related_query_name влияют на поведение запросов.

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

  • Урок 1.1: типы полей и параметры
  • Базовое понимание внешних ключей и JOIN в SQL

Три типа связей

Django реализует связи через три поля: ForeignKey, OneToOneField, ManyToManyField. Каждое из них транслируется в разную структуру на уровне PostgreSQL.


ForeignKey - связь "один ко многим"

Самая распространенная связь. Один объект одной модели связан со многими объектами другой.

В нашем проекте: один Category, много Product, один Order, много OrderItem.

class Product(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name="products",
    )

Что создается в PostgreSQL:

-- Колонка с суффиксом _id
ALTER TABLE shop_product ADD COLUMN category_id bigint NOT NULL;

-- Внешний ключ
ALTER TABLE shop_product
    ADD CONSTRAINT shop_product_category_id_fk
    FOREIGN KEY (category_id)
    REFERENCES shop_category(id)
    DEFERRABLE INITIALLY DEFERRED;

-- Индекс на колонке внешнего ключа (создается автоматически)
CREATE INDEX shop_product_category_id ON shop_product (category_id);

Важные детали

Имя колонки. Django добавляет суффикс _id к имени поля. Поле category в Python → колонка category_id в БД. Обратиться к id напрямую можно через product.category_id, это не делает лишнего запроса, в отличие от product.category.id.

Индекс создается автоматически. ForeignKey всегда создает индекс на колонке внешнего ключа. Это нужно для эффективных JOIN и поиска по обратной связи.

DEFERRABLE INITIALLY DEFERRED. Django создает отложенный внешний ключ, его проверка происходит в конце транзакции, а не при каждой операции. Это позволяет, например, создавать связанные объекты в любом порядке внутри одной транзакции.

Параметр on_delete

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

# PROTECT - запрещает удаление, если есть связанные объекты
category = models.ForeignKey(Category, on_delete=models.PROTECT)

# CASCADE - удаляет вместе с родительским объектом
order = models.ForeignKey(Order, on_delete=models.CASCADE)

# SET_NULL - устанавливает NULL (требует null=True)
parent = models.ForeignKey("self", null=True, on_delete=models.SET_NULL)

# SET_DEFAULT - устанавливает default
assigned_to = models.ForeignKey(User, on_delete=models.SET_DEFAULT, default=1)

# SET(...) - устанавливает конкретное значение или результат callable
manager = models.ForeignKey(User, on_delete=models.SET(get_default_manager))

# DO_NOTHING - не делает ничего (опасно, нарушает целостность)
log_entry = models.ForeignKey(Order, on_delete=models.DO_NOTHING)

Что происходит в БД при CASCADE:

-- Django добавляет ON DELETE CASCADE к foreign key constraint
ALTER TABLE shop_orderitem
    ADD CONSTRAINT shop_orderitem_order_id_fk
    FOREIGN KEY (order_id)
    REFERENCES shop_order(id)
    ON DELETE CASCADE;

При PROTECT и SET_NULL, Django обрабатывает логику на уровне Python, а не на уровне БД. Это означает, что при удалении объекта Django сначала делает SELECT для поиска связанных объектов, затем выполняет нужные действия. Это влечет дополнительные запросы.

Единственное исключение CASCADE, если все связи по цепочке используют CASCADE, Django может выполнить удаление одним запросом SQL с ON DELETE CASCADE.

Параметр related_name

class Product(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.PROTECT,
        related_name="products",  # имя обратной связи
    )

related_name задает имя атрибута для обратного доступа, от Category к Product:

category = Category.objects.get(slug="phones")

# С related_name="products"
category.products.all()

# Без related_name - Django генерирует имя автоматически
category.product_set.all()

# НЕ работает при заданном related_name="products":
# category.product_set.all()  # AttributeError
# product_set доступен только если related_name не задан вообще

related_name также используется в lookups через двойное подчеркивание:

# Найти категории, у которых есть активные продукты дороже 500
Category.objects.filter(products__is_active=True, products__price__gt=500)

Если related_name="+"- обратная связь отключается. Это полезно, когда у вас есть несколько связей с одной и той же моделью, и вы не планируете использовать обратные запросы для одной из них. Это предотвращает ошибки именования при выполнении миграций.

# Обратная связь не нужна
created_by = models.ForeignKey(User, on_delete=models.PROTECT, related_name="+")

Доступ к связанным объектам

product = Product.objects.get(id=1)

# Прямой доступ - делает SELECT к таблице shop_category
category = product.category

# Доступ к id без JOIN - читает уже загруженное поле category_id
category_id = product.category_id  # никакого запроса

# Обратная связь - делает SELECT к таблице shop_product
products = category.products.all()
-- product.category
SELECT * FROM shop_category WHERE id = 1;

-- category.products.all()
SELECT * FROM shop_product WHERE category_id = 1;

Каждое обращение к product.category в цикле, отдельный запрос. Это проблема N+1, которую мы детально разберем в уроке 4.3.


OneToOneField - связь "один к одному"

Частный случай ForeignKey с дополнительным UNIQUE constraint. Один объект связан ровно с одним другим объектом.

Применение: профиль пользователя, настройки магазина, расширение стандартной модели.

from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name="profile",
    )
    phone = models.CharField(max_length=20, blank=True)
    address = models.TextField(blank=True)

Что создается в PostgreSQL:

ALTER TABLE shop_userprofile ADD COLUMN user_id bigint NOT NULL;

ALTER TABLE shop_userprofile
    ADD CONSTRAINT shop_userprofile_user_id_key UNIQUE (user_id);

ALTER TABLE shop_userprofile
    ADD CONSTRAINT shop_userprofile_user_id_fk
    FOREIGN KEY (user_id) REFERENCES auth_user(id) ON DELETE CASCADE;

Отличие от ForeignKey, добавляется UNIQUE constraint, который гарантирует уникальность на уровне БД.

Доступ работает в обе стороны:

user = User.objects.get(id=1)

# Прямой доступ к профилю (SELECT к shop_userprofile)
profile = user.profile

# Если профиля нет - RelatedObjectDoesNotExist (подкласс ObjectDoesNotExist)
try:
    profile = user.profile
except UserProfile.DoesNotExist:
    profile = None

# Или с помощью hasattr
if hasattr(user, "profile"):
    profile = user.profile

ManyToManyField - связь "многие ко многим"

Один объект связан со многими объектами другой модели и наоборот.

В нашем проекте такой связи пока нет, добавим для демонстрации: теги для продуктов.

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True)

    def __str__(self):
        return self.name


class Product(models.Model):
    # ... остальные поля ...
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name="products",
    )

Выполните миграции.

Что создается в PostgreSQL:

-- Промежуточная таблица (join table)
CREATE TABLE shop_product_tags (
    id bigint NOT NULL PRIMARY KEY,
    product_id bigint NOT NULL REFERENCES shop_product(id),
    tag_id bigint NOT NULL REFERENCES shop_tag(id),
    UNIQUE (product_id, tag_id)  -- пара уникальна
);

CREATE INDEX ON shop_product_tags (product_id);
CREATE INDEX ON shop_product_tags (tag_id);

Django создает промежуточную таблицу автоматически. Имя формируется как appname_modelname_fieldname. Она содержит только внешние ключи и уникальное ограничение на пару.

Работа с M2M

product = Product.objects.get(id=1)
tag_sale = Tag.objects.get(slug="sale")
tag_new = Tag.objects.get(slug="new")

# Добавить теги
product.tags.add(tag_sale, tag_new)

# Убрать тег
product.tags.remove(tag_sale)

# Установить конкретный набор (удалит лишние, добавит новые)
product.tags.set([tag_sale])

# Очистить все теги
product.tags.clear()

# Получить все теги продукта
product.tags.all()

# Обратная связь - все продукты с этим тегом
tag_sale.products.all()

SQL для product.tags.add(tag_sale, tag_new):

INSERT INTO shop_product_tags (product_id, tag_id) VALUES (1, 3), (1, 7)
ON CONFLICT DO NOTHING;

SQL для product.tags.all():

SELECT shop_tag.*
FROM shop_tag
INNER JOIN shop_product_tags ON shop_tag.id = shop_product_tags.tag_id
WHERE shop_product_tags.product_id = 1;

Фильтрация M2M

# Продукты с тегом "sale"
Product.objects.filter(tags__slug="sale")

# Продукты с обоими тегами (AND)
Product.objects.filter(tags__slug="sale").filter(tags__slug="new")

# Продукты хотя бы с одним из тегов (OR через Q)
from django.db.models import Q
Product.objects.filter(Q(tags__slug="sale") | Q(tags__slug="new"))

SQL для фильтрации M2M всегда включает JOIN с промежуточной таблицей:

-- filter(tags__slug="sale")
SELECT DISTINCT shop_product.*
FROM shop_product
INNER JOIN shop_product_tags ON shop_product.id = shop_product_tags.product_id
INNER JOIN shop_tag ON shop_product_tags.tag_id = shop_tag.id
WHERE shop_tag.slug = 'sale';

Through model - промежуточная модель с данными

Иногда в связи M2M нужно хранить дополнительные данные. Например, дата добавления тега или кто его добавил:

class ProductTag(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_at = models.DateTimeField(auto_now_add=True)
    added_by = models.ForeignKey(
        User, on_delete=models.SET_NULL, null=True
    )

    class Meta:
        unique_together = [("product", "tag")]


class Product(models.Model):
    tags = models.ManyToManyField(
        Tag,
        through="ProductTag", # промежуточная модель с данными
        related_name="products",
    )

При использовании through стандартные методы add(), remove(), set(), clear() становятся недоступны, управление связью происходит через явное создание объектов ProductTag:

ProductTag.objects.create(
    product=product,
    tag=tag_sale,
    added_by=request.user,
)

Подробно through models разберем в уроке 4.4.


Самореференция (рекурсивные связи)

В нашем проекте Category имеет родительскую категорию того же типа:

class Category(models.Model):
    parent = models.ForeignKey(
        "self",           # ссылка на саму себя
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name="children",
    )

"self" строковая ссылка, эквивалентная Category. Используется, чтобы не ссылаться на класс до его определения.

SQL аналогичен обычному ForeignKey:

ALTER TABLE shop_category ADD COLUMN parent_id bigint;
ALTER TABLE shop_category
    ADD CONSTRAINT shop_category_parent_id_fk
    FOREIGN KEY (parent_id) REFERENCES shop_category(id);

Работа с деревом категорий:

# Корневые категории (без родителя)
Category.objects.filter(parent__isnull=True)

# Дочерние категории для "Electronics"
electronics = Category.objects.get(slug="electronics")
electronics.children.all()

# Все продукты категории и её подкатегорий - через два уровня
Category.objects.filter(
    Q(slug="electronics") | Q(parent__slug="electronics")
)

Для глубоких иерархий (дерево неограниченной глубины) рекурсивные ForeignKey неэффективны, нужны специализированные решения: django-mptt, django-treebeard или рекурсивные CTE с помощью raw SQL. Это выходит за рамки курса, но важно знать о ситуации.


Ленивые строковые ссылки

Если модели определены в разных файлах или ссылаются друг на друга, используйте строковую ссылку:

# Вместо импорта класса
from shop.models import Category  # может вызвать circular import

# Строковая ссылка - Django разрешит её при старте
category = models.ForeignKey("shop.Category", on_delete=models.PROTECT)

# Для модели из того же приложения без указания app
category = models.ForeignKey("Category", on_delete=models.PROTECT)

Сравнение типов связей

ForeignKey OneToOneField ManyToManyField
SQL Foreign Key FK + UNIQUE Промежуточная таблица
Индекс Автоматически Автоматически Два индекса в join table
Доступ "назад" category.products.all() user.profile tag.products.all()
on_delete Обязателен Обязателен Не нужен
Уникальность Нет Да (один к одному) Пара уникальна

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

  1. Откройте psql и посмотрите схему таблицы shop_orderitem через \d shop_orderitem. Найдите все внешние ключи и индексы.

  2. Добавьте модель Tag и поле tags в Product (как показано в уроке). Сделай миграцию. Через psql проверьте, какая промежуточная таблица создалась и какие у нее индексы.

  3. В shell выполните:

   from shop.models import Product, Tag
   product = Product.objects.first()
   # Добавьте 2-3 тега с помощью product.tags.add(...)
   # Посмотрите в консоли какие SQL запросы выполнились
  1. Объясните: почему product.category_id не делает запрос SQL, а product.category.id, делает?

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

Забыть related_name при нескольких FK на одну модель

class Order(models.Model):
    user = models.ForeignKey(User, on_delete=models.PROTECT)
    # Если добавить второй FK на User без related_name будет конфликт имен
    approved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    # Django выдаст ошибку: reverse accessor clashes
# Правильно, явно задать related_name для каждого
user = models.ForeignKey(User, on_delete=models.PROTECT, related_name="orders")
approved_by = models.ForeignKey(
    User, on_delete=models.SET_NULL, null=True, related_name="approved_orders"
)

CASCADE там, где нужен PROTECT

# Удаление категории удалит все продукты, скорее всего это не то, что нужно
category = models.ForeignKey(Category, on_delete=models.CASCADE)

# Лучше запретить удаление категории, если есть продукты
category = models.ForeignKey(Category, on_delete=models.PROTECT)

Добавлять дополнительные данные в M2M без through

# Так не работает, нет места хранить added_at
product.tags.add(tag, added_at=timezone.now())  # AttributeError

# Нужна through model с явным полем

Обращение к атрибуту связи как к значению

# product.category - это объект Category (или RelatedManager)
# Не строка, не ID

product.category == "Electronics"  # False, это объект
product.category.id == 1           # Правильно
product.category_id == 1           # Правильно и без лишнего запроса

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

В уроке 1.3 разберем Meta options модели: как задать сортировку по умолчанию, создать составные индексы, добавить constraints и переименовать таблицу в БД. Также рассмотрим, что такое менеджер модели и зачем он нужен, это основа для понимания кастомных менеджеров в уроке 8.


<< Урок 1.1

Урок 1.3 >>


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

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

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

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

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

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

Пишите info@aisferaic.ru

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