Связи моделей: ForeignKey, M2M, OneToOne

Реальные данные связаны: у поста есть автор, у заказа — товары, у статьи — теги. Django выражает эти связи тремя типами полей-отношений.
Суть: ForeignKey — связь «многие к одному», ManyToManyField — «многие ко многим», OneToOneField — «один к одному». Django автоматически создаёт обратные связи.

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

Реляционные базы строятся на связях между таблицами. Django выражает их декларативно:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(
        Author, on_delete=models.CASCADE, related_name="posts"
    )
    tags = models.ManyToManyField("Tag", related_name="posts")

class Profile(models.Model):
    author = models.OneToOneField(
        Author, on_delete=models.CASCADE, related_name="profile"
    )
    bio = models.TextField()

Связи на уровне таблиц выглядят так:

ForeignKey (многие → один):
   Post.author_id ──────▶ Author.id
   много постов у одного автора

ManyToManyField (многие ↔ многие):
   Post ──┐   ┌── промежуточная таблица ──┐   ┌── Tag
          └──▶│  post_id  │  tag_id        │◀──┘
   у поста много тегов, у тега много постов

OneToOneField (один ↔ один):
   Profile.author_id ─── уникально ───▶ Author.id

ForeignKey и on_delete

Самая частая связь — ForeignKey. Она хранит ссылку на одну запись родительской таблицы. Обязательный параметр on_delete говорит, что делать при удалении родителя: CASCADE — удалить и потомков, PROTECT — запретить удаление, SET_NULL — обнулить ссылку (нужно null=True). Это важное решение о целостности данных, поэтому Django заставляет указать его явно.

Обратные связи и related_name

Django автоматически создаёт «обратную» сторону связи. Если у Post есть author, то у объекта автора появляется доступ ко всем его постам. Параметр related_name="posts" задаёт имя этого обратного доступа:

author = Author.objects.get(pk=1)
author.posts.all()        # все посты автора (обратная связь)
post.author               # автор поста (прямая связь)
post.tags.all()           # теги поста (M2M)
post.tags.add(tag)        # добавить тег

Как это работает под капотом

Под капотом ForeignKey — это просто столбец author_id с числом, ссылающимся на первичный ключ. Связывание объектов через общий ключ — языко-независимая операция «join». Вот как она выглядит на чистом Python:

# Попробуй сам ▶ — join по внешнему ключу вручную
authors = {1: "Анна", 2: "Борис"}
posts = [
    {"title": "Django ORM", "author_id": 1},
    {"title": "Шаблоны",    "author_id": 1},
    {"title": "Формы",      "author_id": 2},
]

# прямая связь: post.author
for p in posts:
    print(f'{p["title"]:14} автор: {authors[p["author_id"]]}')

print("---")

# обратная связь: author.posts.all()
from collections import defaultdict
reverse = defaultdict(list)
for p in posts:
    reverse[p["author_id"]].append(p["title"])

for aid, name in authors.items():
    print(f'{name}: {reverse[aid]}')

Django делает это эффективнее — через SQL JOIN, но идея ровно та же: одна таблица хранит ключ, другая по нему находится.

ManyToMany и промежуточная таблица

Связь «многие ко многим» нельзя выразить одним столбцом, поэтому Django создаёт скрытую промежуточную таблицу с парами ключей. Если нужны дополнительные поля у связи (например, дата добавления тега), используют параметр through с собственной моделью-связкой.

Частые ошибки

  • Забыть on_delete у ForeignKey. Это обязательный параметр, без него ошибка.
  • Выбрать CASCADE там, где нужен PROTECT. Удаление автора молча снесёт все его посты.
  • Не задать related_name. Тогда обратная связь зовётся post_set — менее читаемо, и возможны конфликты.
  • Делать запрос в цикле по связям (N+1). Используйте select_related/prefetch_related.

Best practices

  • Всегда задавайте осмысленный related_name в множественном числе для FK и M2M.
  • Обдуманно выбирайте on_delete: для критичных данных — PROTECT.
  • Используйте select_related для ForeignKey и prefetch_related для ManyToMany, чтобы избежать N+1.
  • Для M2M с доп. полями применяйте through-модель.

Итоги

Три типа связей покрывают любые отношения данных: ForeignKey (многие-к-одному), ManyToMany (многие-ко-многим), OneToOne (один-к-одному). Django создаёт обратные связи, а related_name делает их читаемыми. Параметр on_delete определяет целостность. Дальше — как смотреть данные через админку и оптимизировать запросы.

Проверьте себя
1. Какую связь использовать: у поста один автор, у автора много постов?
AOneToOneField на посте
BManyToManyField
CForeignKey на посте, указывающий на автора
DIntegerField с id автора
2. Зачем нужен параметр related_name у ForeignKey?
AЗадаёт имя таблицы в базе
BЗадаёт читаемое имя обратной связи (author.posts вместо author.post_set)
CШифрует ссылку
DУскоряет миграции