2019/07/15
Django モデル クエリセット ManyToMany【Django】ManyToManyフィールド throughで中間テーブルを自作する
Djangoで多対多の関係を作りたいときはManyToManyフィールドを使いますが、その引数としてthrough
を使うことができます。
through
を使えば独自の中間テーブルを使うことができます。その使い方についてです。
ManyToManyFieldを使った時の参照や逆参照の仕方は、こちらの記事で書いています。
ManyToManyフィールドを使うと、中間テーブルは自動生成される
まずは、through
オプションを使わない単純なManyToManyFieldの例からです。
ManyToManyFieldを使うと、Djangoは自動で中間テーブルを作成してくれます。(中間テーブルのテーブル名は、モデル名とフィールド名から自動で命名されます。db_table
オプションを使えば、自分で命名することもできます。)
models.py
from django.db import models
class Person(models.Model):
"""人"""
name = models.CharField("名前", max_length=100)
class Team(models.Model):
"""チーム"""
name = models.CharField("チーム名", max_length=100)
members = models.ManyToManyField("Person")
Team
クラスのmembers
フィールドでManyToManyFieldを使いPerson
モデルを参照する設定にしています。
これにより、PersonとTeamを紐づける中間テーブルが自動生成されます。
上のようなモデルを作った場合は、以下のようなオブジェクト操作が可能です。
オブジェクト操作
>>> p1 = Person.objects.create(name='Jobs')
>>> team_A = Team.objects.create(name='チームA')
# team_AのメンバーとしてJobsを登録する
>>> team_A.members.add(p1)
# team_Aに所属する全てのメンバーを取得する
>>> team_A.members.all()
# Jobsが所属する全てのチームを取得する
>>> p1.team_set.all()
中間テーブルのフィールド名
自動生成された中間テーブルは、3つのフィールドを持ちます。
他モデルに対してリレーションを作った場合(今回の例のような場合)
- id : リレーションの主キーフィールド
- 参照元モデル_id(team_id) : ManyToManyFieldを使用しているモデルのインスタンスが保存されるフィールド
- 参照先モデル_id(person_id) : ManyToManyフィールドで指定された参照先モデルのインスタンスが保存されるフィールド
自身のモデルに対してリレーションを作った(参照元モデル=参照先モデル)の場合
- id: リレーションの主キー
- from_モデル_id: 参照元インスタンスのid
- to_モデル_id: 参照先インスタンスのid
throughで独自の中間テーブルを作る
ManyToManyフィールドを使えば中間テーブルが自動生成されると書きましたが、through
引数を使えば、自分で作ったモデルを中間テーブルとして使うことができるようになります。
上で紹介したように、自動生成された中間テーブルは3つしかフィールドを持ちませんが、独自の中間テーブルではフィールドを自由に追加するなどのカスタマイズが可能になります。
独自の中間テーブルを使うには、以下のようにします。
models.py
class Person(models.Model):
"""人"""
name = models.CharField("名前", max_length=100)
class Team(models.Model):
"""チーム"""
name = models.CharField("チーム名", max_length=100)
members = models.ManyToManyField(
"Person",
through="PersonTeamRelation", # 追加
)
# 追加
class PersonTeamRelation(models.Model):
"""人とチームの中間テーブル"""
team = models.ForeignKey("Team", on_delete=models.CASCADE)
person = models.ForeignKey("Person", on_delete=models.CASCADE)
joined_date = models.DateField()
reason = models.CharField(max_length=64, verbose_name="きっかけ")
PersonTeamRelation
という中間モデルを作り、それをManyToManyFieldのthrough
引数で指定することで、中間テーブルとして使用しています。
中間テーブルを独自実装することで、「人とチームのリレーション」に対してjoined_date
フィールドやreason
フィールドのように必要な情報を付け加えることができます。
オブジェクト操作
>>> from datetime import date
# p1をteam_Aとteam_Bのmemberにする
>>> PersonTeamRelation.objects.create(team=team_A, person=p1, joined_date=date.today(), reason="友達に誘われたから")
>>> PersonTeamRelation.objects.create(team=team_B, person=p1, joined_date=date.today(), reason="友達に誘われたから")
# team_Aのmemberを全て取得
>>> team_A.members.all()
# p1が持つリレーションを全て取得
>>> p1.personteamrelation_set.all()
# p1が所属するチームを、joined_dateと共に全て取得
>>> p1.personteamrelation_set.all().values_list('team', 'joined_date')
<QuerySet [(1, datetime.date(2019, 7, 15)), (2, datetime.date(2019, 7, 15))]>
through_fieldsで参照先フィールドを明示する
上のPersonTeamRelation
モデルの例では、Team
モデルを参照するForeignKeyフィールドが1つ、Person
モデルを参照するForeignKeyフィールドが1つあります。
このように、ForeignKeyを使ったフィールドが2つあると、それぞれの参照先モデルを結びつける多対多の関係を作ってくれます。
では、以下の例ではどうでしょう。Team
を参照するフィールドが1つという点では上と同じですが、Person
を参照するフィールドが2つ(person
, inviter
)あります。
models.py
class Person(models.Model):
"""人"""
name = models.CharField("名前", max_length=100)
class Team(models.Model):
"""チーム"""
name = models.CharField("チーム名", max_length=100)
members = models.ManyToManyField(
"Person",
through="PersonTeamRelation",
)
class PersonTeamRelation(models.Model):
"""人とチームの中間テーブル"""
team = models.ForeignKey("Team", on_delete=models.CASCADE)
person = models.ForeignKey("Person", on_delete=models.CASCADE)
inviter = models.ForeignKey(
"Person",
on_delete=models.CASCADE,
verbose_name="チームへの招待者",
related_name="invited_teams",
)
joined_date = models.DateField()
reason = models.CharField(max_length=64, verbose_name="きっかけ")
person
フィールドとinviter
フィールドがどちらもPerson
モデルを参照しています。
こうなった場合、Djangoはどちらのフィールドを使って多対多の関係を作って良いかが判別できなくなり、エラーとなってします。
エラーを防ぐためには、次のようにthrough_fields
を使います。
models.py
class Person(models.Model):
"""人"""
name = models.CharField("名前", max_length=100)
class Team(models.Model):
"""チーム"""
name = models.CharField("チーム名", max_length=100)
members = models.ManyToManyField(
"Person",
through="PersonTeamRelation",
through_fields=("team", "person") # 追加
)
class PersonTeamRelation(models.Model):
"""人とチームの中間テーブル"""
team = models.ForeignKey("Team", on_delete=models.CASCADE)
person = models.ForeignKey("Person", on_delete=models.CASCADE)
inviter = models.ForeignKey(
"Person",
on_delete=models.CASCADE,
verbose_name="チームへの招待者",
related_name="invited_teams",
)
joined_date = models.DateField()
reason = models.CharField(max_length=64, verbose_name="きっかけ")
through_fields
で明示的に2つのフィールドを指定してあげることによって、指定されたteam
フィールドとperson
フィールドのオブジェクトで多対多の関係を作ってくれます。
オブジェクト操作
>>> p1 = Person.objects.create(name='Jobs')
>>> p2 = Person.objects.create(name='Rola')
>>> team_A = Team.objects.create(name='チームA')
>>> PersonTeamRelation.objects.create(team=team_A, person=p2, inviter=p1, joined_date=date.today(), reason='Jobsに誘われたから')
# inviterがJobsのPersonTeamRelationオブジェクトを全て取得
>>> p1.invited_teams.all()
# Jobsがどのチームにどの人を招待したかの一覧を取得
>>> p1.invited_teams.all().values_list('person', 'team')
<QuerySet [(2, 1)]>