【Django】モデルの紐づけ(ManyToManyField 編)


こんにちは、生涯腰痛持ちのゴマ太郎です。
前回の記事を書いた後、初めてのギックリ腰を経験しました。ギックリなんて痛いだけやん、我慢すれば普通に生活できるやんと舐めていた過去の自分はどこかへ消え去りました。
案件が忙しくなってしまい前回の Django モデルの ForeignKey から半年以上も空いてしまいましたが、今回はそのお仲間の ManyToManyField について解説していきます。

1. ManyToManyField とは

1-1. モデルを紐づけるリレーションフィールド

前回の ForeignKey 編では Django モデルを紐づけるフィールドの例を下表で紹介しました。

フィールド名関係性使用例
ForeignKey一対多学科 - 学科の在籍学生
ManyToManyField多対多講義 - 講義の履修学生
OneToOneField一対一学生 - 学生のロッカー番号

1-2. ManyToManyField は「多対多」の関係を持たせる

ManyToManyField の「多対多」が何を意味するのか説明します。
前節では「講義 - 講義の履修学生」という例を挙げました。



上図が ManyToManyField の例です。「多対多」というと互いが多くのオブジェクトに紐づく「 2 以上 対 2 以上」の関係と思いがちですが、実際のところは「 0 以上 対 0 以上」のような、互いに紐づくオブジェクト個数に制限がない関係を指します。

ここからは実際に「講義 - 講義の履修学生」の例で ManyToManyField の使用方法を紹介していきます。

2. 前提条件

2-1. 環境

  • Ubuntu 20.04 LTS on WSL2
  • VSCode 1.76.0
  • Python 3.10.8
  • Django 4.1.5

3. ManyToMany の使い方

3-1. models.py での書き方

ManyToManyField を使うには以下のように書きます。

from django.db import models


class Lecture(models.Model):
    name = models.CharField(max_length=30)


class Student(models.Model):
    name = models.CharField(max_length=30)
    lecture = models.ManyToManyField(Lecture, blank=True)


ここで ManyToManyField に関係している記述は以下の行になります。
Student モデルの lecture フィールドに、 Lecture モデルをセットしています。

lecture = models.ManyToManyField(Lecture, blank=True)
フィールド名 = models.ManyToManyField(紐づけたいモデル, blank=True)

ForeignKey では「一対多」のうち【多】のモデルに指定するという規則がありましたが、 ManyToManyField ではどちらのモデルにでも指定が可能です。
ただし ManyToManyField を指定した方とそうでない方で、データの操作方法が変わります。操作方法の違い、指定時の考え方についてはこの後の 4-1 節にて参考記事を紹介しています。


3-2. 管理サイトで確認

確認に必要なコードの追記

ここからは ManyToManyField が追加できていることを管理サイトで確認していきます。
その準備としていくつか追記していきます。

class Lecture(models.Model): # 前節で記述済
    name = models.CharField(max_length=30) # 前節で記述済

    def __str__(self) -> str:
        return self.name

from django.contrib import admin
from .models import Lecture, Student


class LectureAdmin(admin.ModelAdmin):

    list_display = ("name", "_num_of_student")

    # _num_of_student: 講義を履修している学生人数
    def _num_of_student(self, obj):
        return len(Student.objects.filter(lecture=obj))


class StudentAdmin(admin.ModelAdmin):
    list_display = ("name", "_lectures")

    # 履修中の講義名を ", " 区切りで連結
    def _lectures(self, obj):
        return ', '.join(lec.name for lec in obj.lecture.all())


admin.site.register(Lecture, LectureAdmin)
admin.site.register(Student, StudentAdmin)

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

モデルを変更しているためマイグレートも必要です。

python manage.py makemigrations
python manage.py migrate

管理サイト

管理サイト~/admin/にアクセスすると、 LecturesStudents が追加されています。


Lectures に入り、講義を追加します。


NUM OF STUDENT という列がありますが、まだ学生の追加をしていないため全て 0 となっています。


今度は Students に入り、学生を追加します。
学生追加画面には学生の名前に加え、先ほど作成した講義のリストも用意されています。
このリストから 1 つ以上の講義を選択して学生を作成すると、学生と講義が紐づけられることになります。



Lectures に戻ってみると、先ほどは全て 0 だった NUM OF STUDENT が、紐づいている学生の人数に増えているはずです。


以上が ManyToManyField の使用方法になります。

4. おまけ

4-1. どちらに指定すべきか

ManyToManyField のデータを扱うとき、 ManyToManyField を指定したモデル(Student)とそれに紐づけられたモデル(Lecture)でデータの扱い方が異なります。

こちらの記事で分かりやすく解説されていましたので、どちらのモデルに ManyToManyField を指定するべきか迷った際には参考にしてみてください。

realizeznsg.hatenablog.com

4-2. 中間テーブルを自作する

前章のやり方で ManyToManyField によってモデル同士を紐づけると、裏側の DB では中間テーブルを自動作成してくれています。この中間テーブルが持つのは紐づけに最低限必要なフィールド(PK / 学生ID / 講義ID)だけです。もし独自のフィールドも追加したいといった場合には through オプションにより中間テーブルを自作することができます。

through オプションで中間テーブルを自作する方法はこちらの記事で解説されています。

djangobrothers.com

5. 最後に

今回は Django での ManyToManyField の実装方法を解説しました。説明していない内容もまだまだありますので、気になった方は Django 公式ドキュメント をご確認ください。

ここまで読んでいただきありがとうございました。
次回 OneToOneField 編も 書くことがありましたら(?) よろしくお願いいたします。お疲れ様でした!

6. 参考