Python: Django ORM

Теория: Многие ко Многим

На предыдущем уроке были рассмотрены связи "один к одному" и "один ко многим". Тогда же был упомянут и вид связи "многие ко многим".

Для этого вида связи тоже есть специальный тип поля: ManyToManyField. Этот вид связи подразумевает, что у объектов обеих моделей будет RelatedManager, отражающий множество связанных сущностей. Более того, это будет отдельный вид менеджера — ManyRelatedManager.

Например, у нас есть следущие модели:

class Tag(models.TimestampedModel):
    name = models.CharField(max_length=30)


class Post(models.TimestampedModel):
    title = models.CharField(max_length=100)
    body = models.CharField(max_length=300)
    tags = models.ManyToManyField(Tag)

У любого поста может быть несколько тегов, а может не быть ни одного. И одним тегом можно пометить более чем один пост. Поэтому пост и тег соотносятся как "многие ко многим". Заметьте, что опция on_delete не указана: кажется неверным удалять посты, если вдруг будет удалён тег, и уж точно не следует удалять теги при удалении помеченного ими поста.

Работают с такого рода связью следующим образом:

intro = Tag.objects.create(name="Introduction")
Post.objects.get(title="Intro").tags.add(intro)
# SELECT "blog_post". ...

# Execution time: 0.000412s [Database: default]
# BEGIN

# Execution time: 0.000046s [Database: default]
# INSERT
#     OR
# IGNORE INTO "blog_post_tags" ("post_id", "tag_id") SELECT 1,
#        1

Заметьте, что вставка производится в таблицу "blog_post_tags" — это вспомогательная таблица, которую создает Django ORM, чтобы связать таблицы "blog_post" и "blog_tag". В проекте для неё вы не найдёте соответствующей модели. Как видите, ORM может скрывать даже части схемы базы и берёт на себя всю работу.

Также обратите внимание на то, как происходит связывание тега и поста: ManyRelatedManager в атрибуте .tags экземпляра модели Post имеет специальный метод .add(). Он принимает произвольное количество тегов с которыми нужно связать данный пост. В свою очередь, со стороны модели Tag также есть ManyRelatedManager, позволяющий работать с постами, связанными с конкретным тегом.

# Создание нескольких тегов
python_tag = Tag.objects.create(name="Python")
django_tag = Tag.objects.create(name="Django")
orm_tag = Tag.objects.create(name="ORM")

# Создание поста с несколькими тегами
post = Post.objects.create(
    title="Django ORM Tips", body="Useful tips for working with Django ORM..."
)
post.tags.add(python_tag, django_tag, orm_tag)

# Поиск всех постов с определенным тегом
django_posts = Post.objects.filter(tags=django_tag)

# Поиск всех тегов поста
post_tags = post.tags.all()

# Получение постов с несколькими тегами одновременно
advanced_posts = Post.objects.filter(tags__in=[python_tag, orm_tag]).distinct()

Разорвать связь между объектами можно с помощью метода .remove() у любого из двух ManyRelatedManager, передав в аргументах метода перечень тегов для поста и наоборот. Но помните, что сами теги при этом удалены не будут. Объекты, связанные как "многие ко многим" удалять следует с помощью их собственных менеджеров, а не с помощью ManyRelatedManager.

Связь через выделенную модель

По умолчанию на каждый ManyToManyField Django ORM создаст по вспомогательной таблице, в которой будет два столбца типа FOREIGN KEY, которые и будут ссылаться на сущности в связываемых таблицах. Такое неявное использование таблиц удобно в большинстве случаев.

Однако встречаются ситуации, когда факт связи между двумя сущностями хочется сопроводить какой-либо дополнительной информацией. Например, хочется знать, в какой момент времени некий тег был прикреплён к некоторому посту в блоге или какой пользователь этот тег посту присвоил. Здесь пригодилась бы отдельная модель, но не хочется терять все те удобства, которые даёт использование ManyRelatedManager.

Специально для подобных случаев ManyToManyField позволяет с помощью аргумента through указать конкретную модель, которая будет выступать связью. И разумеется, если модель будет указана, то ORM лишнюю таблицу создавать не будет.

Этот подвид связи достаточно интересен, но применяется наиболее редко. Более подробно почитать про особенности использования вспомогательной модели вы сможете в специальном разделе документации.