Организация связей между таблицами

⏱ 30 мин · 🧩 заданий: 7

Зачем нужны связи

Данные почти никогда не живут изолированно: у поста есть автор и комментарии, у товара — категория, у пользователя — профиль. В реляционной базе это выражается связями между таблицами. В Django есть три типа связей.

Один ко многим: ForeignKey

У книги один автор, у автора — много книг:

class Author(models.Model):
    name = models.CharField(max_length=100)
    country = models.CharField(max_length=50, blank=True)

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")

В базе появится столбец author_id с внешним ключом.

on_delete — что делать при удалении

Значение Поведение при удалении автора
CASCADE Удалить и все его книги
PROTECT Запретить удаление, пока есть книги (ProtectedError)
SET_NULL Записать NULL в author (нужно null=True)
SET_DEFAULT Записать значение по умолчанию
DO_NOTHING Ничего (опасно — нарушится целостность)

Работа со связью

pushkin = Author.objects.create(name="Пушкин", country="Россия")
book = Book.objects.create(title="Евгений Онегин", author=pushkin)

book.author            # объект Author
book.author.name       # "Пушкин"
book.author_id         # число — без лишнего запроса

От автора можно получить его книги через обратный менеджер:

pushkin.books.all()            # благодаря related_name="books"
pushkin.books.count()
pushkin.books.create(title="Капитанская дочка")

Без related_name менеджер назывался бы book_set: pushkin.book_set.all(). Понятное имя делает код читаемее.

Многие ко многим: ManyToManyField

У поста много тегов, у тега много постов:

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


class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(Tag, related_name="posts", blank=True)

Django создаст промежуточную таблицу blog_post_tags с парами (post_id, tag_id).

django = Tag.objects.create(name="django")
python = Tag.objects.create(name="python")

post.tags.add(django, python)   # добавить
post.tags.remove(python)        # убрать
post.tags.set([django])         # заменить набор целиком
post.tags.clear()               # убрать все
post.tags.all()                 # теги поста
django.posts.all()              # посты с тегом (обратная сторона)

Если в промежуточной таблице нужны свои поля (например, дата добавления), указывают модель-посредник: ManyToManyField(Tag, through="PostTag").

Один к одному: OneToOneField

Каждому пользователю — ровно один профиль:

from django.conf import settings


class Profile(models.Model):
    user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile")
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to="avatars/", blank=True)
user.profile.bio      # обратная сторона — не менеджер, а сам объект
profile.user.username

Так расширяют встроенную модель пользователя — подробно в модуле про аутентификацию.

Ссылка на модель строкой

Если модель объявлена ниже или в другом приложении, указывайте её строкой: models.ForeignKey("Author", ...) или models.ForeignKey("library.Author", ...). На пользователя ссылаются через settings.AUTH_USER_MODEL.

Запросы через связи

Двойное подчёркивание позволяет «шагать» по связям в фильтрах:

Book.objects.filter(author__name="Пушкин")
Book.objects.filter(author__country="Россия")
Post.objects.filter(tags__name="django")
Author.objects.filter(books__title__icontains="война")   # авторы, у которых есть такие книги

ORM сам построит нужные JOIN.

Проблема N+1

Цикл for book in Book.objects.all(): print(book.author.name) делает отдельный запрос за каждым автором. Решение — select_related("author") для ForeignKey и prefetch_related("tags") для ManyToMany. Подробно — в уроке про оптимизацию запросов.

Итог

  • ForeignKey — один ко многим, ставится на стороне «многих».
  • ManyToManyField — многие ко многим, управление через add/remove/set/clear.
  • OneToOneField — один к одному, например профиль пользователя.
  • on_delete обязателен для ForeignKey и OneToOneField.
  • related_name задаёт имя обратной связи; фильтры по связям — через __.

Практика решено 0 из 7

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

У книги один автор, у автора много книг. Какое поле добавить в модель Book?

Задание 2 Один вариант ответа

Пост может иметь много тегов, тег — много постов. Какая это связь?

Задание 3 Написать код

Опишите модель Comment со связью на Post: при удалении поста комментарии удаляются, а у поста комментарии доступны как post.comments. Добавьте поле body (TextField).

Задание 4 Один вариант ответа

Что делает on_delete=models.PROTECT?

Задание 5 Написать код

Получите все книги, автор которых родился в стране Россия (у модели Author есть поле country, у Book — ForeignKey с именем author). Результат — в books.

Задание 6 Несколько вариантов

Модель Post имеет tags = models.ManyToManyField(Tag). Какие операции корректны?

Задание 7 Ввод ответа

Как по умолчанию (без related_name) называется обратный менеджер у объекта Author для модели Book? (например, author.???)

Войдите, чтобы отмечать пройденные уроки.