DjangoのUserモデルを拡張する方法

Djangoにはデフォルトで次のようなUserモデル(テーブル)と、そのモデルに合わせた認証システムが組み込まれています。

    Column    |           Type           | Collation | Nullable |                Default
--------------+--------------------------+-----------+----------+---------------------------------------
 id           | integer                  |           | not null | nextval('auth_user_id_seq'::regclass)
 password     | character varying(128)   |           | not null |
 last_login   | timestamp with time zone |           |          |
 is_superuser | boolean                  |           | not null |
 username     | character varying(150)   |           | not null |
 first_name   | character varying(30)    |           | not null |
 last_name    | character varying(150)   |           | not null |
 email        | character varying(254)   |           | not null |
 is_staff     | boolean                  |           | not null |
 is_active    | boolean                  |           | not null |
 date_joined  | timestamp with time zone |           | not null |

簡単なアプリケーションであればこれだけで十分事足りるのではないかと思うのですが、物によってはユーザーに紐づく情報を足したり、細かい調整をしたい時があると思います。その中でも、「Djangoのデフォルトの認証機能に満足しているが、認証とは無関係の属性をユーザーに追加したい」というケースにおいて、解決方法をメモします。

シナリオ

認証とは無関係の属性として「性別」と「生年月日」、「住所」を追加し、Userモデルを拡張したいと思います。

拡張方法

調査の結果、Userモデルの拡張方法として次の4つのやり方がありました。

  1. プロキシモデルを利用する(簡単)
  2. Userモデルと1対1でひもづく新たなモデルを作成する(簡単)
  3. AbstractBaseUserからUserモデルをカスタマイズして拡張する(難しい)
  4. AbstractUserからUserモデルをカスタマイズして拡張する(難しい)

プロキシモデルを利用する方法

モデルの継承という概念を使って、データベースに新しくテーブルを作らずにモデルの拡張を実現できます。既存のデータベーススキーマに影響を与えることなく、既存のモデルの動作を変更できるため、気軽に実装できる点がメリットです。

データベースに情報を保存する必要がないケースで利用します。

Userモデルと1対1でひもづく新たなモデルを作成する方法

独自のデータベーステーブルを通常のDjangoモデルとして作成し、 OneToOneField というものを介して既存のユーザーモデルと1対1のリレーションを貼る方法です。

既存のユーザーモデルに対し、認証プロセスに関係しない情報を追加する必要がある場合に便利な方法です。今回はこれを使います。

AbstractBaseUserからUserモデルをカスタマイズして拡張する方法

AbstractBaseUser という既存のUserモデルを継承しつつ、カスタマイズします。既存のデータベース構造を改造することになるので、プロジェクトの初期段階で計画的に導入するのが理想的です。

メールアドレスとパスワードでユーザー認証をしたい場合など、アプリケーションの認証プロセスに関して特定の要件がある場合にこの方法が有効です。

AbstractUserからUserモデルをカスタマイズして拡張する方法

調査中

1対1でひもづく新しいモデルを作ってUserモデルを拡張する

Profileモデルクラスの作成

Profile という名前のモデルを作り、「性別」と「生年月日」のカラムを持たせます。

新しいモデルを作るには、Djangoのアプリケーションディレクトリにある models.py にコードを追記していきます。

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

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    gender = models.CharField(max_length=20, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    location = models.CharField(max_length=30, blank=True)
    favorite_words = models.CharField(max_length=50, blank=True)

さらに signals を定義して、Userインスタンスの作成/更新と同期してプロファイルモデルが自動的に作成/更新されるようにします。

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)
    gender = models.CharField(max_length=20, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    location = models.CharField(max_length=30, blank=True)
    favorite_words = models.CharField(max_length=50, 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メソッドが起動します。

マイグレーションファイルの作成

次のコマンドでマイグレーションファイルを作成します。

python manage.py makemigrations アプリケーション名

すると、マイグレーション可能なものとしてリストアップされ、次のコマンドでチェックできます。

python manage.py showmigrations

マイグレーションの実行

下記コマンドでマイグレーションを実行します。するとマイグレーションファイルの情報に基づいて、DBにテーブルが作成されます。

python manage.py migrate
     Column     |         Type          | Collation | Nullable |                     Default
----------------+-----------------------+-----------+----------+--------------------------------------------------
 id             | integer               |           | not null | nextval('wantedly_app_profile_id_seq'::regclass)
 gender         | character varying(20) |           | not null |
 birth_date     | date                  |           |          |
 location       | character varying(30) |           | not null |
 favorite_words | character varying(50) |           | not null |
 user_id        | integer               |           | not null |

テンプレートに表示する

↓こんな感じでテンプレートに記述して表示できます。お茶の子さいさいですね。

<h2>{{ user.get_full_name }}</h2>
<ul>
  <li>名前: {{ user.username }}</li>
  <li>性別: {{ user.profile.gender }}</li>
  <li>生年月日: {{ user.profile.birth_date }}</li>
  <li>住所: {{ user.profile.location }}</li>
  <li>好きな言葉: {{ user.profile.favorite_words }}</li>
</ul>

フォームから値を受け取ってDBに保存する

例えば新規ユーザー登録(サインアップ)する場合を考えてみます。

HTMLのフォームからPOSTされた値をviews.pyで一旦受け取って、データベースへ保存します。

# views.pyのコード
def sign_up(request):
    if request.method == 'POST':
        username = request.POST['username']
        gender = request.POST['gender']
        birth_date = request.POST['birth_date']
        email = request.POST['email']
        password = request.POST['password']
        user = User.objects.create_user(username, email, password)
        user.profile.gender = gender
        user.profile.birth_date = birth_date
        user.save()
    return render(request, 'registration/sign_up.html')

フォームのメソッドがmethod=“post"のとき、inputフォームに入力された値を格納し、user.save()で最終的にDBへ保存しています。

request.POST[‘hogehoge’]のhogehogeの部分は、inputフォームのname属性と同じにします。

まとめ

データベース周りも少しずつ扱えるようになってきました。やったね!