Blank?=False

ゆるゆる仕事したいフリーランスエンジニアの記事

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を使うことを検討する
  • 参照フィールドは必ず参照・逆参照の両側からテストコード作成。