ManyToManyField で多対多のリレーションを扱う

「1 つの記事に複数のタグを付けられ、1 つのタグが複数の記事に使われる」——このような関係が多対多です。Django では ManyToManyField を使うと、中間テーブルの管理を Django に任せることができます。

基本的な定義

ManyToManyField はどちらのモデルに定義しても動作しますが、意味的に自然な方に置くのが慣習です。

from django.db import models

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

class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    tags = models.ManyToManyField(
        Tag,
        related_name='articles',
        blank=True
    )

この定義だけで、Django は article_tags のような中間テーブルを自動的に作成します。article_idtag_id の 2 つの外部キーを持つシンプルな構造です。

データの追加と削除

多対多の関係は addremoveclearset の 4 つのメソッドで操作します。

python_tag = Tag.objects.create(name='Python')
django_tag = Tag.objects.create(name='Django')
article = Article.objects.create(title='Django入門', body='本文...')

# タグを追加
article.tags.add(python_tag, django_tag)

# タグを 1 つ削除
article.tags.remove(python_tag)

# タグをすべて削除
article.tags.clear()

# タグを指定したものだけに置き換え
article.tags.set([python_tag, django_tag])

addremove は複数のオブジェクトをまとめて渡せます。set は現在の関係をすべて破棄してから指定したものだけを関連付けるため、「これだけにしたい」という場面で便利です。

データの参照

正方向・逆方向のどちらからでもアクセスできます。

# 記事に付いているタグ一覧
article.tags.all()

# あるタグが付いた記事一覧
python_tag.articles.all()
正方向(article.tags)

ManyToManyField を定義した側からのアクセス。all()・filter() 等が使える

逆方向(tag.articles)

related_name で指定した名前でアクセス。使い方は正方向と同じ

filter と組み合わせることで、「特定のタグが付いた公開済み記事」のような条件も簡単に書けます。

Article.objects.filter(
    tags__name='Python',
    is_draft=False
)

リレーション先のフィールドにはアンダースコア 2 つでアクセスします。tags__name は「tags を辿って name フィールドを参照する」という意味です。

中間テーブルをカスタマイズする(through)

中間テーブルに「いつタグを付けたか」などの情報を持たせたい場合は、through パラメータで中間モデルを明示的に定義します。

class Article(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(
        Tag,
        through='ArticleTag',
        related_name='articles'
    )

class ArticleTag(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        unique_together = ('article', 'tag')

through を使う場合、addset は直接使えなくなります。代わりに中間モデルを直接作成します。

ArticleTag.objects.create(
    article=article,
    tag=python_tag
)

unique_together を設定しておくと、同じ記事に同じタグが重複して付くのを防げます。

ManyToManyField を使うべき場面

多対多のリレーションは、設計上「どちらのモデルからも相手の一覧を取得したい」場面で使います。タグ、カテゴリの複数選択、ユーザーのフォロー関係、商品とオプションの組み合わせなどが典型例です。

一方で、中間テーブルに持たせる情報が多くなりすぎる場合は、中間モデルを独立したモデルとして設計し直す方が見通しがよくなることもあります。リレーションの種類を正しく選ぶことが、保守しやすいデータベース設計につながります。