【Django】ForeignKey・ManyToManyでリレーションを組む【Django】

Django

こんにちは、かつコーチです。

前回はモデルの基本フィールドとCRUD操作を解説しました。

今回は、テーブル同士を関連づける「リレーション」の書き方を扱います。

ブログアプリなら「記事とカテゴリ」「記事とタグ」のように、モデル同士がつながる場面は必ず出てきます。

Django 5系のForeignKeyManyToManyFieldの違い、使い分けを一次情報つきで解説します。

リレーションの種類とDjangoでの表現方法

1対多はForeignKey、多対多はManyToManyField

Djangoでリレーションを組むときは、大きく分けて2種類のフィールドを使います。

関係フィールド具体例
1対多(1対N)ForeignKey1つのカテゴリに複数の記事が属する
多対多(N対N)ManyToManyField1つの記事に複数のタグ、1つのタグが複数の記事に付く

「片方から見て複数、もう片方から見ても複数」ならManyToManyField、「片方から見ると複数だが、もう片方から見ると1つ」ならForeignKeyと覚えると判断しやすいです。

なぜリレーションの理解が重要なのか

リレーションの組み方を間違えると、後からマイグレーション(モデルの変更をデータベースに反映する仕組み)をやり直す羽目になります。

設計の初期段階でどちらを使うべきか判断できるようになっておくと、手戻りを大きく減らせます。

ForeignKeyの基本の書き方

モデル定義とon_delete

ForeignKeyは、関連先のモデルとon_deleteの指定が必須です。

# models.py
from django.db import models

class Category(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    category = models.ForeignKey(
        Category,
        on_delete=models.CASCADE,
        related_name="articles",
    )

    def __str__(self):
        return self.title

on_delete=models.CASCADEは「参照先のカテゴリが削除されたら、紐づく記事も一緒に削除する」という意味です。

on_deleteにはほかにも選択肢があり、用途によって使い分けます。

on_delete挙動
CASCADE参照先が消えたら自分も削除
PROTECT参照先が使われている限り削除できないようにする
SET_NULL参照先が消えたらフィールドをNULLにする(null=Trueが必須)
SET_DEFAULT参照先が消えたらデフォルト値を設定する

related_nameで逆参照を使いやすくする

related_name="articles"を指定すると、カテゴリ側からcategory.articles.all()のように記事一覧を取得できます。

category = Category.objects.get(name="Django")
category.articles.all()  # このカテゴリに属する記事の一覧

related_nameを省略すると、Djangoは自動でarticle_setという名前を割り当てますが、明示的に指定しておいたほうがコードの意図が伝わりやすくなります。

ManyToManyFieldでタグ機能を実装する

基本の書き方

多対多の関係は、ManyToManyFieldをどちらか片方のモデルに書くだけで成立します。

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

    def __str__(self):
        return self.name


class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="articles")
    tags = models.ManyToManyField(Tag, related_name="articles", blank=True)

ForeignKeyと違い、ManyToManyFieldにはon_deleteは指定しません(中間テーブルを介して管理されるためです)。

タグの追加・削除の操作方法

ManyToManyFieldは、.add() .remove() .set()といった専用メソッドで操作します。

article = Article.objects.get(pk=1)
django_tag = Tag.objects.get(name="Django")

article.tags.add(django_tag)          # タグを1つ追加
article.tags.remove(django_tag)       # タグを1つ削除
article.tags.set([django_tag])        # タグの内容をまるごと置き換え
article.tags.all()                    # 紐づくタグ一覧を取得

筆者は最初、article.tags = [django_tag]のように直接代入しようとしてエラーになりました。

ManyToManyFieldは保存前のインスタンスに対して直接代入できず、Direct assignment to the forward side of a many-to-many set is prohibited.というエラーメッセージが出ます。

必ず.add().set()を使う点を覚えておいてください。

つまずきやすいポイント:中間テーブルにデータを持たせたい

ManyToManyFieldだけでは足りない場合

「タグを付けた日時も記録したい」のように、リレーション自体に追加情報を持たせたい場合があります。

❌ Before:ManyToManyFieldのままでは中間テーブルに列を追加できない

class Article(models.Model):
    tags = models.ManyToManyField(Tag, related_name="articles")
    # このままでは「いつタグ付けしたか」を保存する場所がない

✅ After:throughオプションで中間モデルを明示する

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


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

throughで中間モデルを指定すると、tagged_atのような追加情報を持たせつつ、多対多の関係を管理できます。

ただしthroughを指定すると.add().set()が使えなくなり、中間モデルを直接ArticleTag.objects.create()する必要がある点には注意してください。

応用:select_relatedとの組み合わせ

リレーションを組んだ後、一覧表示のたびに関連先へ都度アクセスすると、クエリが大量発行されるN+1問題が起こりやすくなります。

articles = Article.objects.select_related("category").prefetch_related("tags")

select_relatedForeignKey側、prefetch_relatedManyToManyField側で使うのが基本の使い分けです。

この使い分けの詳しい仕組みは、次の記事で徹底的に比較しながら解説します。

まとめ

この記事のポイント

  • 1対多はForeignKey、多対多はManyToManyFieldを使う
  • ForeignKeyにはon_deleteの指定が必須(CASCADE PROTECT SET_NULLなど)
  • ManyToManyFieldの操作は.add() .remove() .set()を使う(直接代入は不可)
  • 中間テーブルに追加情報を持たせたいときはthroughオプションを使う

次に読むべき記事

リレーションを組んだ後によく出会う「QuerySetのフィルタの書き方」を次の記事で解説します。

→ 次の記事:QuerySetの基本とフィルタの書き方

タグ: #Django #中級者向け #データベース

タイトルとURLをコピーしました