Сущности предметной области существуют не сами по себе. Они часто зависят друг от друга. На уровне базы данных такие связи задаются через внешние ключи или даже промежуточные таблицы, как в случае связи "многие ко многим". 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 будет жаловаться. Помните об этой возможности!
Post
и User
.Вам ответят команда поддержки Хекслета или другие студенты.
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
Наши выпускники работают в компаниях:
С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.
Зарегистрируйтесь или войдите в свой аккаунт