DjangoでManyToManyFieldの逆参照ができない(他フィールドで同じモデル使用)
どんな問題?
DjangoのモデルでManyToManyフィールドを作成(別フィールドで使っているモデルと同じモデルを使用)
ManyToManyフィールドを作ったモデル側からの参照なら正常にデータが取得できるのに、 逆参照すると正常に取得できない、という問題が発生しました。
かなりハマったので、書いていきます。
具体的なコード
ユーザのモデル、いくつかのユーザモデルを管理するグループモデル グループのオーナーもユーザモデルを参照します。 ※オーナーのみグループ設定を変更できる、という機能とするため
models.py
from django.db import models class User(models.Model): username = models.CharField(max_length=100, null=False) class Meta: db_table = 'users' class Group(models.Model): owner = models.ForeignKey(User, verbose_name="owner",null=False, on_delete=models.CASCADE) name = models.CharField( max_length=100, null=False) members = models.ManyToManyField(User, related_name="groups", related_query_name="group") class Meta: db_table = 'groups'
モデルができたので、python mange.py makemigrations
python mange.py migrate
でマイグレートし、DBにテーブルを作成したら、
Pythonインタプリタで動きを確認します。
>>> from user_group.models import User, Group >>> usr = User.objects.create(username="testuser") >>> grp = Group.objects.create(owner=usr, name="testgroup") >>> grp.members.add(usr) <QuerySet [<User: User object (1)>]> >>> usr.group_set.all() <QuerySet [<Group: Group object (1)>]>
ユーザーを作って、グループを作って,メンバーを追加。 グループモデルからの参照も、ユーザーモデルからの参照もOK。
別のユーザーも作ってみます。
>>> usr2 = User.objects.create(username="testuser2") >>> grp.members.add(usr2) >>> grp.members.all() <QuerySet [<User: User object (1)>, <User: User object (2)>]>
追加したユーザーをtestgroup1に追加して、メンバーを確認、OK。 ユーザー側から逆参照しようとすると…
>>> usr2.group_set.all() <QuerySet []> #どのグループにも所属してない!?
何故かグループに所属していないと言われます。なんでや。 最初に作ったユーザーは正常にグループが表示できているので・・・ なんでや!?
ここでハマりました。
色々やってみて、もしかして?と思ってusr2がオーナーのグループを作成してみました。
>>> grp2 = Group.objects.create(owner=usr2, name="testgroup2") >>> grp2.members.all() <QuerySet []> #メンバーはいません。 >>> usr2.group_set.all() <QuerySet [<Group: Group object (2)>]> #アレ!?
メンバーを追加していないので、グループ側からメンバーを参照しても誰もいません。 ユーザー側から参照すると・・・・なぜかグループが出てきました。
もしかして: 自分がオーナーのグループ一覧を引っ張ってきている
調査結果まとめ
Userモデルからの逆参照でアクセスする場合、ユーザーがownerになっているGroupの一覧を持ってきていました。 なので、自分がownerになっているGroupがないときに取得した場合、何もなかったわけです。
リレーションがカオスってました。
修正
何もエラーが出ないのですが、内部的にownerのUserとMemberのUserが競合していたと思われます。 djangoの内部処理を確認しないとわかりませんが、複数同じモデルの参照フィールドを持つモデルにアクセスする場合、 全部の参照フィールドにrelated_nameを設定していないとより上に定義したフィールドの値を持ってきてしまうのかな、 というのが調査結果です。 ※そもそも,related_nameを設定しているのにgroup_setでアクセスできていたのがおかしい?
競合を解決するためにownerにも related_name
を追加することで解決します。
models.py
<省略> class Group(models.Model): owner = models.ForeignKey(User, verbose_name="owner",null=False, related_name="group_owner", on_delete=models.CASCADE) <省略>
related_nameを設定したため、逆参照の方法がusr.group_set
からusr.groups
に変わります。
>>> usr.groups.all() <QuerySet [<Group: Group object (1)>]> #見つかった!
コレで解決!
テストコード大量修正になりました・・・ 参照、逆参照どちらもテストコードを書いておいたほうがいいですね。
他の解決方法
manyTomanyフィールドを使わないで、
members管理用のモデル group_members
を作ってForeginKeyフィールド2つで管理する、という方法もあります。
manage.py
class Group_Members(models.Model): group = ForeginKey(Group, on_delete=models.CASCADE) user = ForeginKey(User, on_delete=models.CASCADE)
ただ、コレでできるテーブルはManyToManyフィールドで作ったときと全く同じものになります。 単純にN:Nのテーブルを作るだけならやる意味はありませんが、
例えば、グループのメンバーにロールやステータスをつけたい、といったときはこの方法が必要になってくると思います。
まとめ
- ForeginKey, ManyToManyを使うときはrelated_nameを使うことを検討する
- 参照フィールドは必ず参照・逆参照の両側からテストコード作成。