Django Extend UserModel

Python,TIPSDjango

Djangoにはデフォルトでユーザー認証システムが組み込まれておりそのまま活用することが可能です。
しかし作成するWEBアプリによっては更に多くの情報を保持させたい場合もあります。
この投稿では実際にどのようにUserModelを拡張していくのかを紹介します。

UserModelの拡張方法

一般的な拡張方法としては4種類ほど考えられます。
各方法ごとに説明とどんな時に使用するかを記載します。

a.) Proxy UserModelを使用する

・Proxy UserModelとは
データベースに新しいテーブルは作成せずにModelを継承します。
この手法は既存のデータベーススキーマに影響を与えずにUserModelの動作を変更したい際に採用します。

・どんなときに使用するべきか
データベースに追加の情報を登録できない場合やその必要がない時はProxyModelを使用します。
単純にメソッドを追加するだけの場合やデフォルトの順序付けをしたい際も使用します。

b.) 1対1Modelを使用する

・1対1Modelとは
通常のModelを使用します。 UserModelを拡張するための専用Modelを作成します。
OnetoOneFiledを使用してUserModelと1対1の関係を構築します。

・どんな時に使用するべきか
認証そのものには関連しない追加情報を保存したい際に採用します。
ユーザープロファイルとも呼ばれます。
Djangoの標準UserModelを使用して作成したアプリに追加でユーザ情報を持たせたくなった時にも活用できます。
標準UserModel → カスタムUserModelへの切り替えは厳しいので後から対応したい際は主にこの方式を使用します。

c.) AbstractBaseUserを継承したUserModelを使用する

・AbstractBaseUserとは
AbstractBaseUserを継承して作成したUserModelです。
settings.pyを介して認証プロセスの参照先を更新します。 データベーススキーマに大きな影響をもたらしますので基本的にプロジェクト作成時の一番最初に行うことになります。
柔軟性が非常に高いのが特徴ですがコード量は多くなります。

・どんな時に使用するべきか
認証プロセスに特定の要件がある場合はCustomUserModelを使用する必要があります。
ユーザー名ではなくメールアドレスを認証に使用する際などが該当します。

d.) AbstractUserを継承したUserModelを使用する

・AbstractUserとは
AbstractUserを継承して作成したUserModelです。
settings.pyを介していくつかの参照先を更新します。 データベーススキーマに影響をもたらしますので基本的にプロジェクト作成時の一番最初に行うことになります。
柔軟性は低いですがコード量は少なく済みます。

・どんな時に使用するべきか
属性の追加変更のみをしたい場合はAbstractUserを使用し、それに収まらない場合はAbstractBaseUserを使用します。
ただし、基本的にどんな時でもAbstractBaseUserを使用しることをオススメします。

Proxy UserModelの実装例

Proxy UserModelは既存のUserModelを拡張する方法としては最も簡単に行えます。
デメリットも特にありませんが、同時に使用用途もかなり限られます。

例えば以下の様な使い方をします。

from django.contrib.auth.models import User
from .managers import PersonManager

class Person(User):
    objects = PersonManager()

    class Meta:
        proxy = True
        ordering = ('first_name', )

    def hoge_hogehoge(self):
        ...

PersonというProxyModelを定義しました。 Metaクラス内にproxy = Trueというプロパティを追加することでDjangoにこれがプロキシモデルであることを伝えます。
この場合、デフォルトの順序を再定義しModelにCustomManagerの割り当てと新しいメソッド “do_hogehoge" を定義しました。

User.objects.all()とPerson.objects.all()は同じテーブルに対してクエリを実行しますがProxyModelに対して定義した動作のみ異なった処理がされます。

1対1Modelの実装例

UserModelに関する追加情報を格納する新しいModelを作成します。
注意点としてこの関連データを取得するためには追加のクエリもしくは結合が必要となり、基本的に関連データにアクセスする際はDjango側で追加のクエリを実行します。
しかしこれは"select_related"を使用することで回避できます。事前に関連データにアクセスする必要があることがわかっているので一度のクエリでプリフェッチし対応します。

from django.db import models
from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)


そしてUserModelのインスタンスを作成もしくは更新するときにProfileModelが自動的に作成もしくは更新されるよう以下の様にデコレータを使用します。

from django.db import models
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    instance.profile.save()

上記の例ではsaveイベントが発生するたびに “create_user_profile"メソッドと “save_user_profile"メソッドをUserModelにフックします。

そして使い方です。

<h2>{{ user.get_full_name }}</h2>
<ul>
  <li>ユーザー名: {{ user.username }}</li>
  <li>住所: {{ user.profile.location }}</li>
  <li>誕生日: {{ user.profile.birth_date }}</li>
</ul>

views側はこうします。

def update_profile(request, user_id):
    user = User.objects.get(pk=user_id)
    user.profile.bio = 'ホゲほげhope'
    user.save()

一般的にProfileのsave()は直接呼び出さず、UserModelを介して処理します。
それとFormを使用する場合は以下の様にして複数のフォームを一度に処理できます。

forms.py

class UserForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ('first_name', 'last_name', 'email')

class ProfileForm(forms.ModelForm):
    class Meta:
        model = Profile
        fields = ('url', 'location', 'company')

views.py

@login_required
@transaction.atomic
def update_profile(request):
    if request.method == 'POST':
        user_form = UserForm(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if user_form.is_valid() and profile_form.is_valid():
            user_form.save()
            profile_form.save()
            messages.success(request, _('Your profile was successfully updated!'))
            return redirect('settings:profile')
        else:
            messages.error(request, _('Please correct the error below.'))
    else:
        user_form = UserForm(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
    return render(request, 'profiles/profile.html', {
        'user_form': user_form,
        'profile_form': profile_form
    })

profile.html

<form method="post">
  {% csrf_token %}
  {{ user_form.as_p }}
  {{ profile_form.as_p }}
  <button type="submit">Save changes</button>
</form>

AbstractBaseUserを継承したUserModelの実装例

認証要素としてメールアドレスを使用する必要があるとします。
そしてDjangoのデフォルトAdminを使わないのでis_staffフラグも必要ないとします。

この要件を満たすUserModelは以下の様に作成します。

from __future__ import unicode_literals

from django.db import models
from django.core.mail import send_mail
from django.contrib.auth.models import PermissionsMixin
from django.contrib.auth.base_user import AbstractBaseUser
from django.utils.translation import ugettext_lazy as _

from .managers import UserManager


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(_('email address'), unique=True)
    first_name = models.CharField(_('first name'), max_length=30, blank=True)
    last_name = models.CharField(_('last name'), max_length=30, blank=True)
    date_joined = models.DateTimeField(_('date joined'), auto_now_add=True)
    is_active = models.BooleanField(_('active'), default=True)
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)

    objects = UserManager()

    USERNAME_FIELD = 'email'
    REQUIRED_FIELDS = []

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')

    def get_full_name(self):
        '''
        Returns the first_name plus the last_name, with a space in between.
        '''
        full_name = '%s %s' % (self.first_name, self.last_name)
        return full_name.strip()

    def get_short_name(self):
        '''
        Returns the short name for the user.
        '''
        return self.first_name

    def email_user(self, subject, message, from_email=None, **kwargs):
        '''
        Sends an email to this User.
        '''
        send_mail(subject, message, from_email, [self.email], **kwargs)


AbstractBaseUserを継承しているのでいくつかの規則があります。

  • USERNAME_FIELD
    一意の識別子として使用されるためこのフィールドはユニークである必要があります
  • REQUIRED_FIELDS
    creatinguperuserコマンドでユーザーを作成するときに要求されるフィールド名のリスト
  • is_active
    ユーザーを「アクティブ」と見なす際に使用される属性です
  • get_full_name()
    ユーザーのより正式な識別子です
    一般的な解釈はユーザーのフルネームですがユーザーを識別できる任意の文字列にすることもできます
  • get_short_name()
    ユーザーの短い非公式識別子です
    一般的な解釈はユーザーの苗字等です。

そして “create_user"メソッドや"create_superuser"メソッドを定義する場合は以下の様なUserManagerを作成します。

from django.contrib.auth.base_user import BaseUserManager

class UserManager(BaseUserManager):
    use_in_migrations = True

    def _create_user(self, email, password, **extra_fields):
        """
        Creates and saves a User with the given email and password.
        """
        if not email:
            raise ValueError('The given email must be set')
        email = self.normalize_email(email)
        user = self.model(email=email, **extra_fields)
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_user(self, email, password=None, **extra_fields):
        extra_fields.setdefault('is_superuser', False)
        return self._create_user(email, password, **extra_fields)

    def create_superuser(self, email, password, **extra_fields):
        extra_fields.setdefault('is_superuser', True)

        if extra_fields.get('is_superuser') is not True:
            raise ValueError('Superuser must have is_superuser=True.')

        return self._create_user(email, password, **extra_fields)

最後にsettings.pyを更新する必要があります。
AUTH_USER_MODELのプロパティに作成したカスタムUserModelを割り当てます。

AUTH_USER_MODEL = 'users.User'


これはDjangoにカスタムUserModelをデフォルトの代わりに使用するよう定義しています。
今回は “users" というアプリ名でカスタムUserModelを作成しています。
アプリとして独立させておくことで他のプロジェクトで再利用が簡単になりダンプも取得できるためです。

では早速このカスタムUserModelを参照してみましょう。

from django.db import models
from django.conf import settings

class HogeHoge(models.Model):
    name = models.CharField(max_length=100)
    key = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)


key = models.ForeignKey(User, on_delete=models.CASCADE)

直接UserModelを指定しても問題はありませんが、再利用可能なアプリを作るのであればsettings.pyからAUTH_USER_MODELの定義を引っ張ってくることをオススメします。

AbstractUserを継承したUserModelの実装例

django.contrib.auth.models.AbstractUserはデフォルトのUserの完全な実装を抽象しているのでとても簡単に実装できます。

from django.db import models
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    skill = models.TextField(max_length=500, blank=True)
    location = models.CharField(max_length=30, blank=True)
    birth_date = models.DateField(null=True, blank=True)

既存Userから項目を削除することはできませんがこれだけで3つの要素を追加できました。
それとAbstractBaseUserと同様にsettings.pyへ定義を追加する必要があります。

AUTH_USER_MODEL = 'users.User'

AbstractBaseUserと同じようにデータベーススキーマ全体が変更されるためプロジェクト作成時、一番最初に行うべきです。
そして前述した様にカスタムUserModelを直接参照するのではなく、settings.pyからsettings.AUTH_USER_MODELを参照するUserModelに外部キーを作成することをお勧めします。

まとめ

状況や要件に合わせて4つの中からベストな手法を選択してください。
ただし、プロジェクトの開始時には原則AbstractBaseUserを継承したUserModelを作成しておくことを強くオススメします。

最後に今回紹介したパターンを要約します。

  • Proxy UserModel
    デフォルトのUserModelが提供する機能の全てに不足がなく追加の情報を保存する必要もない場合にオススメ
  • UserProfile
    デフォルトのUserModelが提供する認証機能には不足がないが認証関連以外の属性を追加したい場合にオススメ
  • AbstractBaseUser
    デフォルトのUserModelが提供する認証機能が全く自分のプロジェクトに合わないときにオススメ
  • AbstractUser
    デフォルトのUserModelが提供する認証機能には不足がないがそれでも別のModelを作成せずに属性の追加をしたい場合にオススメ

Python,TIPSDjango

Posted by Kenny