【Python・Django】AbstractUserを2回使いたい時の対処法


環境

Python:3.65 Django:2.1 Database:Postgresql

AbstractUserを2回使いたい

具体例

サービスAのクライアントユーザーの管理はこっちのDB(スキーマ)で、管理者ユーザーは別で管理したい…! ユーザー側のシステムと、管理側のシステムが切り離されている場合切り離されたシステム(通常利用画面、ユーザー管理画面など)毎にDjangoのアプリが存在しそれに伴いDB(スキーマ)も別となっている場合があるかと思います。 そのような場合、 ・Aというアプリにもユーザー・権限管理が存在 ・Bというアプリでもユーザー・権限管理が存在 というような状況が起こりえます。

正直どちらかのDB(スキーマ)内でユーザー管理を統一し、2種類のユーザーを1モデルで処理するほうが楽なのですが、そうは行かない場合は一工夫して上げる必要があります。 (https://torina.top/detail/295/

問題点

AbstractUser(AbstractBaseUserでも同様)はPermissionMixinというものが継承されたクラスとなっています。 その影響で、以下のようにAbstractUserを2回継承した場合は、

from django.contrib.auth.models import AbstractUser

User(AbstractUser):
    pass

AnotherUser(AbstractUser):
    pass

こんな感じで全力で怒られます。辛い・・・

ERRORS: abstractuser_test.AnotherUser.groups: (fields.E304) Reverse accessor for ‘AnotherUser.groups’ clashes with reverse accessor for ‘User.groups’. HINT: Add or change a related_name argument to the definition for ‘AnotherUser.groups’ or ‘User.groups’. abstractuser_test.AnotherUser.user_permissions: (fields.E304) Reverse accessor for ‘AnotherUser.user_permissions’ clashes with reverse accessor for ‘User.user_permissions’. HINT: Add or change a related_name argument to the definition for ‘AnotherUser.user_permissions’ or ‘User.user_permissions’. abstractuser_test.User.groups: (fields.E304) Reverse accessor for ‘User.groups’ clashes with reverse accessor for ‘AnotherUser.groups’. HINT: Add or change a related_name argument to the definition for ‘User.groups’ or ‘AnotherUser.groups’. abstractuser_test.User.user_permissions: (fields.E304) Reverse accessor for ‘User.user_permissions’ clashes with reverse accessor for ‘AnotherUser.user_permissions’. HINT: Add or change a related_name argument to the definition for ‘User.user_permissions’ or ‘AnotherUser.user_permissions’.

ここで問題となっている部分は、上述の通りAbstractUserが継承しているPermissionMixinの機能となります。 このPermissionMixinはDjango上で簡易的な権限管理を行うための機能ですが、ちょっと扱いづらい。。

・ユーザーがどのグループに属するかの管理(Groupモデルとの多対多) ・個別ユーザーごとの権限の管理(Permissionモデルとの多対多) (上記2つは依存していないため、整合性を取る取らないも含め選択可能です、)

と、いうわけで同じオブジェクトに対して多対多の関係が定義されています。 具体的には以下なのですが、この中で定義されいる、 ・related_name ・related_query_name というのが、先程のエラーの内容ですね。

class PermissionsMixin(models.Model):
# 略
    groups = models.ManyToManyField(
        Group,
        verbose_name=_('groups'),
        blank=True,
        help_text=_(
            'The groups this user belongs to. A user will get all permissions '
            'granted to each of their groups.'
        ),
        related_name="user_set",
        related_query_name="user",
    )
    user_permissions = models.ManyToManyField(
        Permission,
        verbose_name=_('user permissions'),
        blank=True,
        help_text=_('Specific permissions for this user.'),
        related_name="user_set",
        related_query_name="user",
    )

    class Meta:
        abstract = True

# 略

さて、ここまでわかれば後はこちらのものですね。

解決方法

・related_name、related_query_nameを変更する →同じ名前が定義されていると、逆引き時に衝突してエラーが出る部分(上記エラー)の回避 ・Group、Permissionを別名自作して、多対多の関係先を変更 →Group、Permissionを変更しないと、自動で作られる方(1つ目のUserが参照すべきテーブル)の情報を参照してしまうので、自分で振り分ける。

以下のような形で記載を行えば、AbstractUserを2つ継承していても動くはずです…

class User(AbstractUser):
    pass

class AnthoerGroup(models.Model):
    # Groupモデルの内容をコピペ、Metaに以下追加
    class Meta:
        db_table = "hoge"
    
    
class AnthoerPermission(models.Model):
    # Permissionモデルの内容をコピペ、Metaに以下追加
    class Meta:
        db_table = "hoge"


class AnotherUser(AbstractUser):
    #PermissionMixinのgroups,user_permissionsから下記をコピペ
    #related_name,related_query_nameだけ変更
    groups = models.ManyToManyField(
        AnthoerGroup,
        verbose_name=_('groups'),
        blank=True,
        help_text=_(
            'The groups this user belongs to. A user will get all permissions '
            'granted to each of their groups.'
        ),
        related_name="another_user_set",
        related_query_name="another_user",
    )
    user_permissions = models.ManyToManyField(
        AnthoerPermission,
        verbose_name=_('user permissions'),
        blank=True,
        help_text=_('Specific permissions for this user.'),
        related_name="another_user_set",
        related_query_name="another_user",
    )

 

参考

https://docs.djangoproject.com/ja/2.1/topics/auth/default/#permissions-and-authorization

この問題解決してかなりDjangoの理解が深まった気がします。疲れたけど。。 本当はもう少し別の問題もあったのですが、それは中々微妙な解決方法をした気がしているので、一旦記載は控えておきます。

今回の記事の内容は、AbstractUserの継承はDjango自体が1回しか想定していない作りで、Djangoの不文律を犯す内容に近いため出来ればやらないほうがいい内容です。。 まあ検索しても全然出てこなかったので、かなりレアな悩みだと思われますが…