Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Связи Python: Django ORM

Сущности предметной области существуют не сами по себе. Они часто зависят друг от друга. На уровне базы данных такие связи задаются через внешние ключи или даже промежуточные таблицы, как в случае связи "многие ко многим". ORM, в свою очередь, используют эти ключи для работы со связями, а также добавляют множество полезных методов, которые упрощают работу с зависимыми сущностями: выборкой, добавлением, модификацией и удалением.

Учебный проект моделирует предметную область системы для ведения персональных блогов. Сейчас нам интересна модель Post — модель записи (поста) в блоге. Пользователи (модель User) связаны с постами "один ко многим":

  • Один пользователь может быть автором множества постов
  • У каждого поста всегда один автор

Описание связи между моделями

Для того чтобы связать пост и автора, было использовано поле типа ForeignKey, которое в терминах баз данных представляет собой внешний ключ. Ниже соответствующий фрагмент:

class User(models.Model):
    """A blog user."""
    # ...

class Post(models.Model):
    """A blog post."""

    # ...
    creator = models.ForeignKey(User, on_delete=models.CASCADE)
    # ...

Такого описания достаточно для Django ORM: он будет знать, какие запросы следует сделать в БД, чтобы при обращении к полю creator экземпляра модели Post вы получали уже экземпляр пользователя. При этом ORM экономит ресурсы системы и по умолчанию не пытается получить сразу все возможные данные: пока вы не запросите доступ к автору поста, автор не будет запрошен из базы!

Опция on_delete=models.Cascade описана ниже.

Создание связей между объектами

При создании объекта модели, имеющей внешний ключ, в качестве значения поля обычно указывают уже созданный (сохранённый в БД) экземпляр другой модели. Такое возможно, например, при создании объектов с помощью менеджера — вы можете даже создавать несколько связанных объектов в одном выражении:

Post.objects.create(
    title="Intro",
    body="Hi, my name is Bob!",
    creator=User.objects.create(
        email="bob@blogs.org",
        first_name="Bob",
        last_name="Smith",
    )
)
# INSERT INTO "blog_user" ...

# INSERT INTO "blog_post" ...

# Execution time: 0.004678s [Database: default]
# <Post: Post object (1)>
# в учебном проекте выполняемые запросы показывает shell_plus!

Пример показывает, что сначала был создан объект User, а потом уже зависящий от него объект Post. В этом случае вычисление аргументов вызова функции или метода перед вызовом самого метода приходится очень кстати. Впрочем, можно было и заранее создать или запросить User и уже потом передать в вызов Post.objects.create().

Передача экземпляра в роли значения поля не всегда бывает удобна. Порой вы будете иметь на руках только id связанного объекта и делать запрос .get(id=id) будет не слишком оправдано. В таких ситуациях можно использовать автоматически генерируемое поле с тем же именем, что и у внешнего ключа, но с суффиксом _id: это поле принимает в качестве значения id объекта:

post2 = Post.objects.create(
    title="Update", body="I'm tired...", creator_id=1,
)
# INSERT INTO "blog_post" ...

# Execution time: 0.010233s [Database: default]
# <Post: Post object (2)>

post2.creator_id
# => 1

Поле xyz_id доступно не только при создании объекта, но имеется и у загружаемых из базы объектов. При обращении к этому полю запрос к связанной таблице не производится: это поле физически расположено в таблице текущей модели и является тем самым столбцом типа FOREIGN KEY, который бы вы использовали, если бы писали SQL вручную.

Получение данных из связанных моделей

С точки зрения поста запрос данных пользователя максимально прост: вы просто обращаетесь к полям вложенного объекта. При первом обращении к любым данным пользователя будет выполнен запрос в базу, затем данные пользователя будут запомнены и новых запросов ORM делать не будет:

post = Post.objects.get(id=1)
# SELECT "blog_post"."id", ...
# Execution time: 0.000424s [Database: default]
post.creator.email
# SELECT "blog_user"."id", ...
# Execution time: 0.000472s [Database: default]
# 'bob@blogs.org'

post.creator.first_name
# => 'Bob'

С полем Post.creator, кажется, всё понятно. Но как получить доступ к постам пользователя, или, проще говоря, сделать запрос в обратную сторону? Всегда остаётся возможность написать что-то вроде Post.objects.filter(creator=...). Но авторы Django ORM предусмотрели и более простой способ:

bob = User.objects.get(first_name='Bob')
# SELECT "blog_user"."id", ...
# Execution time: 0.000516s [Database: default]
bob.post_set
# <django.db.models.fields.related_descriptors.create_reverse_many_to_one_manager.<locals>.RelatedManager object at 0x7f220fb0d198>
for post in bob.post_set.all():
    print(post.id, post.title)

# SELECT "blog_post"."id", ...

# Execution time: 0.000427s [Database: default]
# => 1 Intro
# => 2 Update

Как видно, у объекта модели User имеется атрибут post_set, значением которого выступает объект типа <..длинное имя..>.RelatedManager. Не стоит переживать из-за имени типа: это лишь менеджер, но менеджер связанной модели. Работать с этим менеджером можно так же, как с любым другим — накладывать фильтры, сортировку. Разница будет заключаться только в том, что в запросах всегда будет присутствовать условие, выбирающее лишь посты конкретного пользователя.

Что же касается имени атрибута, соответствующего RelatedManager, то по умолчанию оно получается прибавлением к имени связанной модели в нижнем регистре суффикса _set. Но можно при описании поля указать опцию related_name="желаемое_имя", тогда у связанной модели атрибут получит уже заданное вами имя.

Удаление связанных сущностей

Связь "один ко многим" никак не ограничивает удаление отдельных постов пользователя. Можно даже удалить все посты некоторого пользователя запросом some_user.post_set.delete(). Или удалить посты, удовлетворяющие некоторому условию, если перед вызовом .delete() как-то ограничить выборку.

А вот удаление пользователя могло бы привести к нарушению целостности базы данных. Поэтому по умолчанию Django ORM не позволяет выполнить такие опасные действия: при попытке выполнить запрос вы получите ошибку.

Чаще всего удаление пользователя подразумевает удаление и всех его постов, поэтому Django ORM позволяет указать опцию on_delete=models.CASCADE в описании внешнего ключа. Объекты с такой связью будут автоматически удалены при удалении "родительского" объекта. Существуют и другие варианты реакции на удаление родителя, обо всех возможностях вы можете почитать в документации к ForeignKey (ссылка).

Связь "один к одному"

Кроме связей вида "один ко многим" существуют ещё связи "один к одному" и "многие ко многим". Последний вид связи мы рассмотрим позже, а пока рассмотрим связь "один к одному".

Этот вид связи в Django ORM реализуется с помощью типа OneToOneField и характерен лишь тем, что у парной модели будет сгенерирован не RelatedManager, а аналог поля, ссылающийся (и запрашивающий) один связанный объект.

Примеров связей "один к одному" в учебном проекте нет, потому что в целом такой вид связи встречается редко. Но вы можете себе представить или попробовать реализовать ситуацию, при которой пользователю предоставляется возможность описать собственную автобиографию. С одной стороны автобиография не обязательна, поэтому OneToOneField будет описано в модели User и будет иметь параметр null=True. При этом в рамках заполняемой автобиографии часть информации — несколько полей — будет обязательной к заполнению. Описать в рамках одной модели требования вроде "эти поля обязательны только вместе" гораздо сложнее, чем завести новую модель-дополнение и привязать к первой "один к одному".

Ссылки на себя

ForeignKey и его производные вроде OneToOneField позволяют модели сослаться на саму себя. Такие ссылки позволяют закодировать родственные связи между людьми или, скажем, описать модель для хранения в БД древовидных структур. И ORM сделает использование таких структур достаточно удобным!

Однако как при описании поля — атрибута класса — сослаться на сам класс, ведь на момент выполнения кода, описывающего поля, класс ещё не существует? Тут на помощь приходит возможность указывать имена моделей в виде строки "self".

Кроме того, строкой можно сослаться и на модель, если указать её полное имя: "full.app_name.ModelName". Этот приём позволяет делать ссылки между моделями, находящимися в разных приложениях, не делая между модулями перекрёстные импорты. В больших проектах такие импорты могут превратиться в циклические, на которые Python будет жаловаться. Помните об этой возможности!


Самостоятельная работа

  1. Изучите связь между Post и User.
  2. Откройте REPL и создайте несколько постов.
  3. Попробуйте описать запросы вида "первый пост пользователя", "три последних поста пользователя", "как давно пользователь ведёт блог" (разница между "created_at" первого поста и текущей датой).

Дополнительные материалы

  1. Связи между объектами

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы
профессия
от 25 000 ₸ в месяц
Разработка веб-приложений на Django
10 месяцев
с нуля
Старт 21 ноября

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»