このページでは、Django における「OneToOneField
を利用した User
の拡張」について解説をしていきます。
Django ではユーザーを管理するモデルとして User
が標準で用意されており、この User
モデルを利用してユーザーのログイン等を実現することが可能です。
このログインについては下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。
【Django】ログイン機能の実現方法(関数ベースビュー編)ただし、User
にはユーザーを管理する際の一般的なフィールドしか存在しませんので、フィールドを追加したくなる場合が多いです。また、無駄なフィールドを削除したくなる場合も多いです。
そんな時には、ユーザーを管理するモデルとして新たなカスタムユーザーを用意することで、開発するアプリに合わせた独自のユーザーモデルを利用することも可能です。
カスタムユーザーについては下記の2つのページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。
【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractUser編】 【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractBaseUser編】上記のカスタムユーザーを用意する方法では、新たなユーザー管理モデルを定義し、そのモデルを直接変更する方法で独自のユーザーモデルを実現していました。
具体的には、AbstractUser
や AbstractBaseUser
等を継承するモデルを新たに定義し、そのモデルをカスタマイズ(フィールドを追加したり削除したり)することで独自のユーザーモデルを実現していました。
それに対し、今回は標準で用意されている User
に他のモデルとのリレーションを構築し、これによって User
の拡張を行うことで独自のユーザーモデルを実現していきたいと思います。
今回は User
に対して他のモデルとのリレーションを構築するために OneToOneField
を利用していきます。これにより、ユーザー管理モデル(User
)を変更することなく、ユーザー管理モデルから新たなフィールドを扱うことができるようになります。
Contents
OneToOneField
を利用した User
の拡張
では、OneToOneField
を利用した User
の拡張手順について解説していきます。
OneToOneField
を利用した User
の拡張手順
OneToOneField
を利用して User
の拡張を行う際の手順の流れは下記のようになります。
User
に追加したいフィールドを持つモデルを定義する- 定義したモデルと
User
とのリレーションを設定するOneToOneField
を利用して設定する
- インスタンス同士のリレーションを構築する
User
から定義モデルへアクセスする- 管理画面での管理対象への定義モデルの追加する
まず、User
に追加したいフィールドを持つモデルを models.py
に新たに定義します。
次に、定義したモデルに OneToOneField
のフィールドを追加することで、そのモデルと User
とのリレーションを設定します。
さらに、追加したフィールドに User
のインスタンスをセットすることで、定義したモデルのインスタンスと User
のインスタンスとの間に実際にリレーションを構築します。
これにより、User
から models.py
に定義したモデルのインスタンス、さらに定義したモデルのインスタンスの各フィールドにアクセスすることができるようになります。ですので、後は必要に応じてフィールドへのデータの格納やフィールドからのデータの取得を行ないながらアプリを開発していくことになります。
また、定義したモデルを管理画面から管理できるようにしたい際には、admin.py
を変更する必要があります。
以上が、OneToOneField
を利用して User
の拡張を行う際の手順の流れとなります。ここからは、各手順について、具体例を踏まえながら実装する必要のある処理等について解説していきます。
スポンサーリンク
User
に追加したいフィールドを持つモデルを定義する
では、まずは「User
に追加したいフィールドを持つモデルの定義」の具体的手順について説明していきます。
今回は、User
に身長(cm)を示すフィールド height
、体重(kg)を示すフィールド weight
、更にログイン回数を管理するフィールド login_num
の3つのフィールドを追加したい場合の例を考えながら、手順について説明していきます。
また、身長と体重は共にユーザーの身体情報を表すフィールドになりますが、ログイン回数に関しては身体情報を表すものでは無いため、これらは別のモデルとして定義していきたいと思います。具体的には、前者のモデルは Profile
、後者のモデルは Activity
として定義していきたいと思います。
これらのフィールドにおいてポイントになるのが「フィールドの値の決め方」になります。
ユーザーの情報や身長と体重に関してはユーザーしか情報を知らないため、ユーザーに値を入力してもらう必要があります。それに対し、ログイン回数に関してはユーザーから入力されてしまうと正しいログイン回数とならないため、ユーザーから入力してもらうのではなく、アプリ側で自動的に値を決定するような処理が必要となります。
このような違いがあるため、Profile
と Activity
とでインスタンスの生成の仕方が異なります。この辺りの実例に関しては、OneToOneField で拡張した User の利用例 で紹介したいと思います。
さて、ここから本題の「User
に追加したいフィールドを持つモデルの定義」の具体的手順について説明していきますが、この手順は簡単で、まずは通常通り models.py
にモデルを定義すれば良いだけになります。
今回は前述の通り、height
フィールドと weight
フィールドを持つ Profile
と login_num
フィールドを持つ Activity
の2つのモデルを定義します。
具体的には、下記のように models.py
を作成することになります。
class Profile(models.Model):
height = models.FloatField(default=0.0)
weight = models.FloatField(default=0.0)
class Activity(models.Model):
login_num = models.IntegerField(default=0)
これにより、User
に追加したいフィールドを持つ Profile
と Activity
が作成されたことになります。
リレーションを設定する
ただし、まだ User
と Profile
および Activity
は全く関連性がなく、それぞれが独立したモデルとなっています。
続いて、これらのモデルに関連性を持たせるため、OneToOneField
を利用して User
と Profile
および、User
と Activity
との間でリレーションを設定していきます。
これは、下記のように、追加したモデルに対して引数 User
を指定した OneToOneField
のフィールドを持たせることで実現することができます。
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
height = models.FloatField(default=0.0)
weight = models.FloatField(default=0.0)
class Activity(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
login_num = models.IntegerField(default=0)
上記のように Profile
と Activity
を変更することで、User
と Profile
の間に1対1の関係、さらに User
と Activity
の間に1対1の関係を持たせることが可能となります。
OneToOneField
の on_delete
は関連するインスタンスが削除された場合の動作を指定するオプションになります
上記のように on_delete=models.CASCADE
を指定した場合、関連するインスタンスが削除された場合に一緒に本モデルのインスタンスも削除されることになります
インスタンス同士のリレーションを構築する
あくまでも、モデルに OneToOneField
のフィールドを持たせることはモデル同士の間に1対1のリレーションの設定を行なっているだけです(リレーションの構築を可能にしているだけ)。つまり、実際に各モデルのインスタンス同士のリレーションを構築するためには、別途処理が必要となります。
このページでは、上記のようなモデル同士で「リレーションの構築を可能にすること」を「リレーションを設定する」と呼ばせていただいています
それに対し、ここから説明するようなインスタンス同士で「実際にリレーションの構築を行うこと」を「リレーションを構築する」と呼ばせていただいています
このページで用いる「リレーションを設定する」と「リレーションを構築する」という言葉では意味合いが異なるので注意してください
そのリレーションを構築するために必要な処理とは、インスタンスの OneToOneField
として追加したフィールドに「リレーションを構築したいインスタンス」をセットする処理になります。
例えば、ここまで用いてきた例で考えれば、Profile
と Activity
には OneToOneField
として user
フィールドを追加していますので、Profile
や Activity
のインスタンスの user
フィールドに対して User
のインスタンスをセットする処理が必要になります。
そして、このインスタンスのセットにより、Profile
と User
のインスタンス同士のリレーション、もしくは Activity
と User
のインスタンス同士のリレーションが構築されることになります。
ただし、セットできるインスタンスは OneToOneField
の第1引数(to
引数)に指定したモデルのものだけである点に注意してください。今回は OneToOneField
の第1引数に User
を指定しているため、Profile
と Activity
の user
フィールドにセットできるのは User
のインスタンスのみとなります。
例えば、views.py
で下記のような処理を実行すれば、User
のインスタンスである user_1
と Profile
のインスタンスである profile_X
に、更に user_1
と Activity
のインスタンスである activity_Y
の間に1対1のリレーションが構築されることになります(models.py
で Profile
と Activity
が定義されている前提の例になります)。
from django.contrib.auth.models import User
from .models import Profile, Activity
user_1 = User(username='ユーザー1')
profile_X = Profile()
activity_Y = Activity()
profile_X.user = user_1
activity_Y.user = user_1
User
と Profile
と Activity
のインスタンスを生成しているのが下記3行になります。
user_1 = User(username='ユーザー1')
profile_X = Profile()
activity_Y = Activity()
ここでは単にインスタンスを生成しているだけであり、これらのインスタンスには何の関係性もありません。
これらのインスタンスの間にリレーションを構築している(インスタンスに関係性を持たせている)のが下記2行になります。
profile_X.user = user_1
activity_Y.user = user_1
1行目で profile_X
と user_1
の間にリレーションが構築され、2行目で activity_Y
と user_1
の間にリレーションが構築されることになります。
上記では直接 User()
や Profile()
を実行して User
のインスタンスと Profile
のインスタンスを生成していますが、実際には入力フォーム等でユーザーに指定された文字列や値に応じたインスタンスを生成することになると思います。
この辺りの実際の処理については OneToOneField で拡張した User の利用例 で紹介したいと思います。
スポンサーリンク
User
からの定義モデルへアクセスする
先程の処理によってインスタンス同士にリレーションが構築されたことになります。
さらに、今回は OneToOneField
を利用してリレーションを設定しており、この場合、インスタンス同士で1対1のリレーションが構築されることになります。
この1体1のリレーションが構築された互いのインスタンスからは、他方のインスタンスに対してデータ属性(フィールド)からアクセスすることができるという特徴があります。
例えば下記によって profile_X
と user_1
にリレーションを構築した場合、
profile_X.user = user_1
profile_X
から user_1
に対して、profile_X.user
によりアクセスすることができます。これは、Profile
モデルに user
フィールドを持たせているので当然の話ではあります。
例えば、下記のように User
モデルのフィールドである username
にアクセスして表示することができます。
print(profile_X.user.username)
さらに、上記のように1対1のリレーションを構築した場合、逆に user_1
から profile_X
にアクセスすることもできます。具体的には、user_1.profile
によってアクセスすることができます。
例えば、下記のように profile_X
に設定された height
と weight
の値を user_1
からアクセスして表示することも可能です。
profile_X.height = 175.2
profile_X.weight = 58.5
print(user_1.profile.height)
print(user_1.profile.weight)
ここでポイントになるのが、User
モデルは変更していないものの、User
モデルのインスタンス(user_1
)に profile
というデータ属性が追加されているという点になります。
User
モデルには元々 profile
というデータ属性は存在しませんが、Profile
側に OneToOneField(User, 略)
のフィールドを持たせておくことで、User
にモデル名(Profile
)を小文字にした profile
データ属性が自動的に追加されることになります。
そして、これによって User
から Profile
のインスタンスにアクセスすることができるようになり、あたかも User
に Profile
の持つフィールドを追加したように動作させることが可能となります。
このように、OneToOneField
を利用し、さらにインスタンス同士にリレーションを構築することで、User
を変更することなく、User
から新たなフィールドを追加で扱うことができるようになります。
上記の例では User
から height
や weight
を扱うことができるようになり、User
に対して height
や weight
のフィールドを追加した時と同様のことをアプリで実現することができるようになります。
もちろん、実際に User
にフィールドを追加しているわけではないため、モデルを介してフィールドにアクセスするようなことが必要になります。この点はちょっと面倒ではあるのですが、User
を変更することなく User
にフィールドを追加したいような場合、OneToOneField
を利用したリレーションの構築は有効だと思います。
管理画面での管理対象への定義モデルの追加
ただし、リレーションを設定していたとしても、互いのモデルは別々のモデルであるという点に注意が必要になります。
例えば、管理画面ではデフォルトで User
のインスタンスの管理(追加や削除・変更など)を行うことが可能になっています。ですが、それ以外のモデルは User
とリレーションが設定されていたとしてもデフォルトでは管理できないようになっています。
つまり、ここまでの例で挙げた Profile
や Activity
のインスタンスの管理に関してはデフォルトでは行うことができません。
そのため、Profile
や Activity
のインスタンスの管理を行いたい場合、これらの管理を行うことができるように管理画面の設定を行う必要があります。
そして、こういった管理画面に関する設定は admin.py
で行うことができ、admin.py
で管理対象に含めたいモデルを引数に指定して admin.site.regiseter
を実行することで、管理画面で管理可能となるモデルの追加を行うことができます。
例えば下記のように admin.py
を変更すれば、管理画面から Profile
や Activity
のインスタンスの管理を行うことができるようになります。
from django.contrib import admin
from .models import Profile, Activity
# Register your models here.
admin.site.register(Profile)
admin.site.register(Activity)
このように変更した場合、管理画面のトップページに Profile
と Activity
のリンクが追加され、
これらのリンク先から各 Profile
・Activity
のインスタンスの管理(変更や削除など)を行うことができるようになります。
リレーションを設定することで関係性は生まれるものの、結局は別々のモデルである点については注意が必要となります。今回は管理画面での管理対象について説明しましたが、views.py
等を実装する際にも注意が必要です。
以上が、OneToOneField
で User
を拡張する際の基本点な流れの説明となります。
OneToOneField
で拡張した User
の利用例
最後に、ここまでのまとめとして OneToOneField
で拡張した User
の利用例を紹介していきます。
今回紹介するのは OneToOneField
で拡張した User
を利用してユーザー登録やログインを行う例となります。
元々の未拡張の User
を利用したログインについては下記ページで解説していますので、ここでは OneToOneField
で User
を拡張した時にポイントとなる点についてのみ解説を行なっていきます。
User
を利用したログインの仕組みをご存知ない方は、別途上記ページを参照していただければと思います。
スポンサーリンク
プロジェクトとアプリの作成
まずは、いつも通りの手順でプロジェクトとアプリを作成していきます。
上記で紹介したページに合わせ、今回はプロジェクト名を login_project
、アプリ名を login_app
にしたいと思います。
そのため、まずは適当なフォルダで下記コマンドを実行してプロジェクトを作成し(念のため言っておきますが、コマンドの先頭の %
に関しては入力不要です)、
% django-admin startproject login_project
さらに、上記コマンドを実行して作成されたフォルダ(login_project
)の中に cd
コマンド等で移動します。以降では、この移動先のフォルダ(login_project
のフォルダ)を作業フォルダとし、ファイルのパスに関しては、このフォルダからの相対パスで示していきます。
作業フォルダに移動した後は、下記のコマンドを実行してアプリを作成します。
% python manage.py startapp login_app
アプリが作成できれば、login_project/settings.py
内の INSTALLED_APPS
を下記のように変更してアプリの登録を行います。
INSTALLED_APPS = [
'login_app', # 追加
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
モデルの定義とマイグレーション
続いてモデルの定義とマイグレーションを行なっていきます。
モデルの定義としては、定義したモデルとの User とのリレーションの構築 で紹介したものをそのまま利用したいと思います。
そのため、login_app/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)
height = models.FloatField(default=0.0)
weight = models.FloatField(default=0.0)
class Activity(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
login_num = models.IntegerField(default=0)
上記によって Profile
と Activity
のモデルが作成され、Profile
と User
の間で、さらに Activity
と User
の間で1対1のリレーションが設定されます。
モデルの定義が完了したので、次は作業フォルダで下記の2つのコマンドを実行してマイグレーションを行いましょう!
% python manage.py makemigrations
% python manage.py migrate
これによって定義したモデル(今回の場合は Profile
と Activity
)や User
に対応するテーブルがデータベースに作成され、それぞれのインスタンスの情報がレコードとして保存可能となります。
後は、View や Template、さらには Form などを作成していくことでログインを実現していきます。
Form・View・Template 等の変更
ということで、次は Form や View 等の変更および作成を行なっていきたいと思います。
ここからは、変更後・作成後のソースコードの紹介および、必要に応じたポイントの説明のみを行なっていきます。
Form
まずは Form を作成していきます。
この Form では「ユーザー登録用入力フォーム」と「ログイン用入力フォーム」を実現するための Form の定義を login_app/forms.py
で行なっていきます(login_app/forms.py
は新規作成が必要なファイルとなります)。
また、ユーザー登録用入力フォームでは、ユーザーのユーザー名・メールアドレス・パスワード(確認用含む)に加え、身長と体重を入力できるようにしていきたいと思います。この身長と体重に対応するフィールド(height
と weight
)は User
ではなく Profile
が持っているものであるという点に注意が必要です。
上記のようなフォームを実現するためには、次のような login_app/forms.py
を作成すれば良いです。
from django import forms
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from .models import Profile
class SignupForm(UserCreationForm):
class Meta:
model = User
fields = ['username', 'email', 'password1', 'password2']
class ProfileForm(forms.ModelForm):
class Meta:
model = Profile
fields = ['height', 'weight']
class LoginForm(AuthenticationForm):
pass
SignupForm
と ProfileForm
の2つが「ユーザー登録用入力フォーム」を実現するための Form であり、LoginForm
が「ログイン用入力フォーム」を実現するための Form になります。
ポイントは、height
と weight
の入力を行えるようにするために ProfileForm
を別途定義している点になります。User
は Profile
とリレーションが設定されているものの、Profile
の持つ height
と weight
のフィールドを持っているわけではありません。
そのため、height
と weight
の入力を行えるようにするために、User
に基づいたフォームとなる SignupForm
だけでなく、ProfileForm
に基づいてフォームとなる ProfileForm
の定義も行っています。
補足しておくと、今回は UserCreationForm
や ModelForm
を継承してモデル毎に Form を定義していますが、これらを継承せずに両者のモデルのフィールドの情報を入力可能な Form を定義してやれば、1つの Form のみの定義で済む可能性もあると思います
ただ、ユーザー登録を実現するのには UserCreationForm
を利用するのが楽なので、上記のように2つの Form を定義するようにしています
Template
次は Template を作成していきます。
Template については login_app/templates/login_app
のフォルダを用意し、このフォルダの中に .html
を作成していく必要がある点に注意してください。
ここでは5つのテンプレートファイルを作成していきたいと思います。それぞれのテンプレートファイルの役割は下記のようになります。
singup.html
:ユーザー登録ページ用のテンプレートlogin.html
:ログインページ用のテンプレートlogout.html
:ログアウトページ用のテンプレートuser.html
:ログイン中のユーザーの情報を表示するページのテンプレートother.html
:ログイン中のユーザー以外の情報をログイン回数に対して降順に並べて表示するページのテンプレート
それぞれのテンプレートファイルの中身は下記のようになります。前述の通り、これらのファイルは全て login_app/templates/login_app
のフォルダの中に作成する必要があります。
{% load static %}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ユーザー登録</title>
</head>
<body>
<h1>ユーザー登録</h1>
<form action="{% url 'signup' %}" method="post">
{% csrf_token %}
<table>
{% for form in forms %}
{{ form.as_table }}
{% endfor %}
</table>
<p><input type="submit" value="登録"></p>
</form>
</body>
</html>
{% load static %}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ログイン</title>
</head>
<body>
<h1>ログイン</h1>
<form action="{% url 'login'%}" method="post">
{% csrf_token %}
{{ form.as_p }}
<p><input type="submit" value="ログイン"></p>
</form>
<p><a href="{% url 'signup'%}">ユーザー登録</a></p>
</body>
</html>
{% load static %}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>ログアウト</title>
</head>
<body>
<p>ログアウトしました...</p>
<p><a href="{% url 'login'%}">ログイン</a></p>
</body>
</html>
{% load static %}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>あなたの情報</title>
</head>
<body>
<h1>あなたの情報</h1>
<table>
<thead>
<tr><th>名前</th><th>ログイン回数</th><th>身長</th><th>体重</th></tr>
</thead>
<tbody>
<tr>
<td>{{user.username}}</td>
<td>{{user.activity.login_num}}</td>
<td>{{user.profile.height}}</td>
<td>{{user.profile.weight}}</td>
</tr>
</tbody>
</table>
<p><a href="{% url 'other'%}">他のユーザーの情報</a></p>
<p><a href="{% url 'logout'%}">ログアウト</a></p>
</body>
</html>
{% load static %}
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>他のユーザーの情報</title>
</head>
<body>
<h1>他のユーザーの情報</h1>
<table>
<thead>
<tr><th>名前</th><th>ログイン回数</th><th>身長</th><th>体重</th></tr>
</thead>
<tbody>
{% for activity in activities %}
<tr>
<td>{{activity.user.username}}</td>
<td>{{activity.login_num}}</td>
<td>{{activity.user.profile.height}}</td>
<td>{{activity.user.profile.weight}}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><a href="{% url 'user'%}">あなたの情報</a></p>
<p><a href="{% url 'logout'%}">ログアウト</a></p>
</body>
</html>
ポイントについて解説しておきます。
1つ目のポイントは singup.html
における入力フォームの表示です。singup.html
では for
ループを利用して views.py
から受け取った複数の入力フォーム(forms
)を表示するようにしています。
なぜ複数の入力フォームを表示するかというと、前述の forms.py
で定義を行なったように、「ユーザー登録用入力フォーム」として User
用の入力フォーム(SignupForm
)と Profile
用の入力フォーム(ProfileForm
)を表示する必要があるためです。今回は、これらの2つの入力フォームを表示する必要があるため、複数の入力フォームを受け取れるようにしています。
1つの入力フォームのみを表示するようにしてしまうと、例えば User
用の入力フォームのみしか表示することができず、Profile
の持つフィールドである height
と weight
の入力が受け付けられなくなるので注意してください。
2つ目のポイントは user.html
と other.html
における User
のインスタンスの情報表示になります。
特に分かりやすいのが user.html
で、user.html
では views.py
から user
として User
のインスタンスを受け取り、user
にリレーションが設定された Profile
と Activity
のインスタンスに user
からアクセスしています。
このアクセスは、これらのモデル名を小文字にした profile
と activity
を user
のデータ属性として指定することで実現しています。
さらに、これらのインスタンスを介して、user.profile.height
や user.profile.weight
によって user
の身長と体重、さらには、user.activity.login_num
によって user
のログイン情報を表示するようにしています。
また、other.html
はログインユーザー以外のユーザーの情報を「ログイン回数」に対して降順に並べて表示するためのテンプレートとなっています。
views.py
からは「ログイン回数に対して降順に並べた Activity
のインスタンス」のクエリーセットである activities
を受け取ることを想定しているテンプレートであり、for
文で activities
から Activity
のインスタンスを activity
として1つずつ抽出し、activity
からユーザー名やログイン回数、ユーザーの身長と体重を表示するようにしています。
ユーザー名に関しては User
モデルの持つフィールドであり(username
フィールド)、さらに Activity
と User
の間にはリレーションが設定されています。そのため、User
の username
には Activity
のインスタンスである activity
からアクセスすることができます。
具体的には、activity.user.username
が activity
とリレーションが構築されている User
のインスタンスのユーザー名となります。なので、ユーザー名の表示は activity.user.username
の表示により実現できます。
また、身長と体重に関しては Profile
の持つフィールドになります。ただし、Activity
と Profile
の間ではリレーションの設定が行われていないため、Activity
から Profile
に直接アクセスすることはできません。
ですが、Activity
と User
の間、更に User
と Profile
の間ではリレーションが設定されているため、Activity
のインスタンスから User
のインスタンスを介して Profile
のインスタンスにアクセスすることができます。したがって、activity
とリレーションが構築されている User
のインスタンスの身長と体重の表示は、activity.user.profile.height
と activity.user.profile.weight
の表示により実現することができます。
このように、Activity
から Profile
の間にリレーションは設定されていませんが、両者とリレーションが設定されている User
を経由することで、Activity
から Profile
のフィールドにアクセスするようなことが可能となります。
もちろん、逆に Profile
から Activity
のフィールドにアクセスすることも可能です。
つまり、直接2つのモデルの間にリレーションが設定されていなくても、お互いのモデルとリレーションが設定されている他のモデルを経由することで、お互いのモデルをデータ属性から辿ることが可能となります。
少しポイントの解説が長くなりましたが、リレーションが設定されているモデルのフィールドへのアクセスの仕方は重要な点なので、是非覚えておいてください!
View
続いて views.py
を変更して View を作成していきます。
今回は View は関数ベースで作成するものとし、5つの関数を views.py
に定義していきたいと思います。それぞれの関数の役割は下記のようになります。
signup_view
:ユーザー登録用の入力フォームの表示および、送信されてきたフォームに基づいたユーザーの登録を行うlogin_view
:ログイン用の入力フォームの表示および、送信されてきたフォームに基づいたユーザーのログインを行うlogout_view
:ログイン中のユーザーのログアウトを行うuser_view
:ログイン中のユーザーの情報を表示するother_view
;ログイン中のユーザーの情報を、ログイン回数に対して降順に並べて表示する
これらを定義する login_app/views.py
の中身は下記のようになります。
from django.shortcuts import render, redirect
from .forms import SignupForm, ProfileForm, LoginForm
from django.contrib.auth import login, logout
from .models import Activity
from django.contrib.auth.decorators import login_required
def signup_view(request):
if request.method == 'POST':
user_form = SignupForm(request.POST)
profile_form = ProfileForm(request.POST)
forms = (user_form, profile_form)
if user_form.is_valid() and profile_form.is_valid:
user = user_form.save()
profile = profile_form.save(commit=False)
profile.user = user
user.profile.save()
activity = Activity()
activity.user = user
activity.save()
login(request, user)
#user.activity.login_num += 1
#user.activity.save()
# 下記でもOK
activity.login_num += 1
activity.save()
return redirect(to='/login_app/user/')
else:
forms = (SignupForm(), ProfileForm())
param = {
'forms': forms
}
return render(request, 'login_app/signup.html', param)
def login_view(request):
if request.method == 'POST':
form = LoginForm(request, data=request.POST)
if form.is_valid():
user = form.get_user()
if user:
login(request, user)
user.activity.login_num += 1
user.activity.save()
return redirect(to='/login_app/user/')
else:
form = LoginForm()
param = {
'form': form,
}
return render(request, 'login_app/login.html', param)
def logout_view(request):
logout(request)
return render(request, 'login_app/logout.html')
@login_required
def user_view(request):
user = request.user
params = {
'user': user
}
return render(request, 'login_app/user.html', params)
@login_required
def other_view(request):
all_activities = Activity.objects.order_by('-login_num')
activities = all_activities.exclude(user=request.user)
params = {
'activities': activities
}
return render(request, 'login_app/other.html', params)
ポイントを3点ほど解説しておきます。
1つ目は各モデルのインスタンスの生成およびインスタンスの保存になります(インスタンスの保存とは、データベース観点で言えばレコードの保存となります)。
これらを行なっているのが signup_view
で、signup_view
では request.method
(リクエストのメソッド)が POST である時に、送信されてきたフォームに基づいて User
と Profile
のインスタンスの生成を行なっています。
この辺りの処理を行なっているのが signup_view
の下記部分になります。
user_form = SignupForm(request.POST)
profile_form = ProfileForm(request.POST)
forms = (user_form, profile_form)
if user_form.is_valid() and profile_form.is_valid:
user = user_form.save()
profile = profile_form.save(commit=False)
profile.user = user
user.profile.save()
まず、SignupForm(request.POST)
と ProfileForm(request.POST)
を実行することで、送信されてきたフォームに基づいて SignupForm
と ProfileForm
のインスタンスが生成されます。
これらはモデルではなくフォームですが、これらのフォームは ModelForm
を継承するフォームであり、ModelForm
を継承するフォームのインスタンス生成時に引数 request.POST
を指定してやることで、送信されてきたフォームの情報に基づいたモデルのインスタンスの生成も行われることになります。
具体的には、ModelForm
はフォーム定義時のクラス変数 model
に指定したモデルに基づいたフォームを生成するクラスであり、ModelForm
を継承するフォームのインスタンスを生成する際(コンストラクタを実行する際)には、その model
に指定されたモデルのインスタンスも生成されることになります。
models.py
で SignupForm
は ModelForm
ではなく UserCreationForm
を継承させる形で定義しており、ModelForm
は関係ないようにも思えるかもしれません
ですが、UserCreationForm
は ModelForm
を継承するフォームなので、ModelForm
と同じ特徴を持っており、前述のようにフォームのインスタンスを生成する際に、その model
に指定されたモデルのインスタンスも生成されることになります
ただし、SignupForm(request.POST)
と ProfileForm(request.POST)
の返却値はフォームのインスタンスであり、モデルのインスタンスではありません。
フォームからのモデルのインスタンスの取得は save
メソッドにより行うことができます。
つまり、下記部分でフォームのインスタンスに save
メソッドを実行させることで、返却値としてモデルのインスタンス(User
のインスタンスと Profile
のインスタンス)の取得を行なっています。
user = user_form.save()
profile = profile_form.save(commit=False)
ここで注目していただきたいのが、上記における save
メソッドの引数の違いです。
save
メソッドの引数として commit=True
を指定する or commit
の引数指定を行わない場合、save
メソッド実行時にモデルのインスタンスの保存が行われます。つまり、上記の前者の行を実行することで User
のレコードがデータベースに保存されることになります。そして、メソッドの返却値として User
のインスタンスが取得できることになります。
それに対し、save
メソッドの引数として commit=False
を指定した場合、save
メソッド実行時にモデルのインスタンスの保存が行われません。つまり、上記の後者の行を実行してもデータベースへの保存が行われません。ただし、メソッドの返却値として Profile
のインスタンスを取得することはできます。
では、わざわざ commit=False
を指定してデータベースへの保存を行わないのはなぜでしょうか?
その理由は、上記の後者の行のタイミングでデータベースへの保存を行うとエラーになるからです。
エラーになるのは、現状の Profile
のインスタンスには必須フィールドの設定が行われていないからです。この必須フィールドとは、models.py
で定義した user
フィールドとなります。
user
フィールドには、必須フィールドではなく任意フィールドにするためのオプション指定をしていないため、user
フィールドは必須フィールドとして扱われます。
ですが、現状の Profile
のインスタンスには user
フィールドには何もセットしていないため、このままデータベースへの保存をしようとするとエラーとなります。
そのため、わざわざ commit=False
を指定して Profile
のインスタンスを取得するために save
メソッドを実行し、取得した Profile
のインスタンスに下記で user
フィールドの設定を行ってから、再度 commit=False
を指定せずに save
メソッドを実行してデータベースへの保存を行うようにしています。
このように、OneToOneField
を必須フィールドとなっている場合、リレーションを構築してからでないとデータベースへの保存時にエラーになるので注意してください。
profile.user = user
user.profile.save()
上記の user
フィールドへの User
のインスタンスのセットにより、Profile
のインスタンスと User
のインスタンスの間に1対1のリレーションが構築され、互いのインスタンスから参照可能な状態になります。
また、User
と Profile
に関してはユーザーからの情報の入力受付を行うためにフォームを用意し、送信されてきたフォームからインスタンスを生成するようにしています。
それに対し、ログイン回数のようなユーザーに情報を入力してもらう必要のないフィールドのみから構成される Activity
に関しては、フォームは用意せず直接コンストラクタ(Activity()
)を実行してインスタンスを生成するようにしています。
さらに、ユーザー登録に成功した場合はログインを行いますが、当然ログインを行なった際にはユーザーのログイン回数が当然1回増えることになります。そのため、ログイン後にインスタンスの login_num
を +1
してからデータベースに保存を行うようにしています。
この辺りの処理を行なっているのが、signup_view
における下記部分になります。
activity = Activity()
activity.user = user
login(request, user)
user.activity.login_num += 1
user.activity.save()
# 下記でもOK
# activity.login_num += 1
# activity.save()
コメントアウトをしていますが、activity.user = user
で activity
と user
の間に1対1のリレーションが構築されているため、”user
の” login_num
は user.activity.login_num
からだけでなく、activity.login_num
からもアクセスすることが可能です。
また、上記は signup_view
での処理になりますが、ログインに関しては login_view
でも行われるため、login_view
でも同様の処理を行なっています。
あとは、通常のログイン時やログイン中のユーザーの情報を表示する時と同様の処理になると思いますので、分からない点があれば下記ページを参照していただければと思います。
【Django】ログイン機能の実現方法(関数ベースビュー編)その他のファイルの変更・作成
ここまでの変更やファイルの作成によって、Django における MVT の部分が完成したことになります。
あとは残りの必要なファイルの変更・作成について紹介していきます。
まず、URL と View の関連付けを行うため、login_app/urls.py
を下記のように新規作成します。
from . import views
from django.urls import path
urlpatterns = [
path('login/', views.login_view, name='login'),
path('logout/', views.logout_view, name='logout'),
path('signup/', views.signup_view, name='signup'),
path('user/', views.user_view, name='user'),
path('other/', views.other_view, name='other'),
]
さらに、https://localhost:8080/login_app/
にアクセスされた際に上記の login_app/urls.py
の関連付けの情報に従って View の関数が実行されるよう、login_project/urls.py
を次のように変更します。
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('login_app/', include('login_app.urls')),
]
また、管理画面から Profile
とActivity
のインスタンスが管理できるようにするため、管理画面での管理対象への定義モデルの追加 での解説に倣って login_app/admin.py
を下記のように変更します。
from django.contrib import admin
from .models import Profile, Activity
# Register your models here.
admin.site.register(Profile)
admin.site.register(Activity)
最後に、ログインしていないユーザーがアプリにアクセスした際にログインページに自動的にリダイレクトされるよう、login_project/settings.py
の一番最後の行に下記を追記します。
LOGIN_URL = '/login_app/login/'
以上が、OneToOneField
によって拡張した User
によるログインを実現するスクリプトの例となります。
ほぼユーザー管理(登録やログイン・ログアウトなど)を行うだけのスクリプトですが、OneToOneField
によって拡張した User
の利用例としては分かりやすいのではないかと思います。
動作自体は下記ページの最後に紹介しているスクリプトと同様になりますので、動作確認したい場合は下記ページを参考にしていただければと思います(next
の利用に関しては上記のスクリプトでは省略させていただいています)。
スポンサーリンク
まとめ
このページでは、Django における「OneToOneField
を利用した User
の拡張」について解説しました。
OneToOneField
を利用することでモデル間に1対1のリレーションを設定することができます。また、リレーションを設定した上で各モデルのインスタンス間で実際にリレーションを構築してやれば、一方のインスタンスから他方のインスタンスを参照することができるようになります。
そして、これらを利用することで、User
を変更しなくても User
から他のモデルのフィールドを参照することができ、User
から新たなフィールドを扱うことができるようになります。
今回は User
を拡張することを目的とした解説になりましたが、もちろん他のモデルを変更することなく拡張したいような場合にも利用できます。
また、User
の拡張にはカスタムユーザーを別途定義する方法もあります。これに関しては下記ページで解説していますので、興味があれば是非読んでみてください!