【Python/Django】OneToOneFieldを利用してUserモデルを拡張する

DjangoにおけるOneTonOneFieldを利用したUserの拡張方法

このページでは、Django における「OneToOneField を利用した User の拡張」について解説をしていきます。

Django ではユーザーを管理するモデルとして User が標準で用意されており、この User モデルを利用してユーザーのログイン等を実現することが可能です。

このログインについては下記ページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

Djangoでのログイン機能の実現方法解説ページアイキャッチ【Django】ログイン機能の実現方法(関数ベースビュー編)

ただし、User にはユーザーを管理する際の一般的なフィールドしか存在しませんので、フィールドを追加したくなる場合が多いです。また、無駄なフィールドを削除したくなる場合も多いです。

そんな時には、ユーザーを管理するモデルとして新たなカスタムユーザーを用意することで、開発するアプリに合わせた独自のユーザーモデルを利用することも可能です。

カスタムユーザーについては下記の2つのページで解説していますので、詳しく知りたい方は下記ページをご参照いただければと思います。

【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractUser編】

カスタムユーザーをAbstractBaseUserを継承して作成する手順の解説ページアイキャッチ【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractBaseUser編】

上記のカスタムユーザーを用意する方法では、新たなユーザー管理モデルを定義し、そのモデルを直接変更する方法で独自のユーザーモデルを実現していました。

具体的には、AbstractUserAbstractBaseUser 等を継承するモデルを新たに定義し、そのモデルをカスタマイズ(フィールドを追加したり削除したり)することで独自のユーザーモデルを実現していました。

AbstractUser等の継承によってユーザーをカスタマイズを行う際のクラス構成

それに対し、今回は標準で用意されている User に他のモデルとのリレーションを構築し、これによって User の拡張を行うことで独自のユーザーモデルを実現していきたいと思います。

リレーションを構築してユーザーのカスタマイズを行う際のクラス構成

今回は User に対して他のモデルとのリレーションを構築するために OneToOneField を利用していきます。これにより、ユーザー管理モデル(User)を変更することなく、ユーザー管理モデルから新たなフィールドを扱うことができるようになります。

OneToOneField を利用した User の拡張

では、OneToOneField を利用した User の拡張手順について解説していきます。

OneToOneField を利用した User の拡張手順 

OneToOneField を利用して User の拡張を行う際の手順の流れは下記のようになります。

  • User に追加したいフィールドを持つモデルを定義する
  • 定義したモデルと User とのリレーションを設定する
    • OneToOneField を利用して設定する
  • インスタンス同士のリレーションを構築する
  • User から定義モデルへアクセスする
  • 管理画面での管理対象への定義モデルの追加する

まず、User に追加したいフィールドを持つモデルを models.py に新たに定義します。

OneToOneFieldを利用したUserの拡張手順1

次に、定義したモデルに OneToOneField のフィールドを追加することで、そのモデルと User とのリレーションを設定します。

OneToOneFieldを利用したUserの拡張手順2

さらに、追加したフィールドに User のインスタンスをセットすることで、定義したモデルのインスタンスと User のインスタンスとの間に実際にリレーションを構築します。

OneToOneFieldを利用したUserの拡張手順3

これにより、User から models.py に定義したモデルのインスタンス、さらに定義したモデルのインスタンスの各フィールドにアクセスすることができるようになります。ですので、後は必要に応じてフィールドへのデータの格納やフィールドからのデータの取得を行ないながらアプリを開発していくことになります。

OneToOneFieldを利用したUserの拡張手順4

また、定義したモデルを管理画面から管理できるようにしたい際には、admin.py を変更する必要があります。

以上が、OneToOneField を利用して User の拡張を行う際の手順の流れとなります。ここからは、各手順について、具体例を踏まえながら実装する必要のある処理等について解説していきます。

スポンサーリンク

User に追加したいフィールドを持つモデルを定義する

では、まずは「User に追加したいフィールドを持つモデルの定義」の具体的手順について説明していきます。

今回は、User に身長(cm)を示すフィールド height、体重(kg)を示すフィールド weight、更にログイン回数を管理するフィールド login_num の3つのフィールドを追加したい場合の例を考えながら、手順について説明していきます。

また、身長と体重は共にユーザーの身体情報を表すフィールドになりますが、ログイン回数に関しては身体情報を表すものでは無いため、これらは別のモデルとして定義していきたいと思います。具体的には、前者のモデルは Profile、後者のモデルは Activity として定義していきたいと思います。

これらのフィールドにおいてポイントになるのが「フィールドの値の決め方」になります。

ユーザーの情報や身長と体重に関してはユーザーしか情報を知らないため、ユーザーに値を入力してもらう必要があります。それに対し、ログイン回数に関してはユーザーから入力されてしまうと正しいログイン回数とならないため、ユーザーから入力してもらうのではなく、アプリ側で自動的に値を決定するような処理が必要となります。

User&ProfileとActivityとのフィールドの値の決め方

このような違いがあるため、ProfileActivity とでインスタンスの生成の仕方が異なります。この辺りの実例に関しては、OneToOneField で拡張した User の利用例 で紹介したいと思います。

さて、ここから本題の「User に追加したいフィールドを持つモデルの定義」の具体的手順について説明していきますが、この手順は簡単で、まずは通常通り models.py にモデルを定義すれば良いだけになります。

今回は前述の通り、height フィールドと weight フィールドを持つ Profilelogin_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 に追加したいフィールドを持つ ProfileActivity が作成されたことになります。

リレーションを設定する

ただし、まだ User と Profile および Activity は全く関連性がなく、それぞれが独立したモデルとなっています。

続いて、これらのモデルに関連性を持たせるため、OneToOneField を利用して UserProfile および、UserActivity との間でリレーションを設定していきます。

これは、下記のように、追加したモデルに対して引数 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)

上記のように ProfileActivity を変更することで、User と Profile の間に1対1の関係、さらに UserActivity の間に1対1の関係を持たせることが可能となります。

MEMO

OneToOneFieldon_delete は関連するインスタンスが削除された場合の動作を指定するオプションになります

上記のように on_delete=models.CASCADE を指定した場合、関連するインスタンスが削除された場合に一緒に本モデルのインスタンスも削除されることになります

インスタンス同士のリレーションを構築する

あくまでも、モデルに OneToOneField のフィールドを持たせることはモデル同士の間に1対1のリレーションの設定を行なっているだけです(リレーションの構築を可能にしているだけ)。つまり、実際に各モデルのインスタンス同士のリレーションを構築するためには、別途処理が必要となります。

MEMO

このページでは、上記のようなモデル同士で「リレーションの構築を可能にすること」を「リレーションを設定する」と呼ばせていただいています

それに対し、ここから説明するようなインスタンス同士で「実際にリレーションの構築を行うこと」を「リレーションを構築する」と呼ばせていただいています

このページで用いる「リレーションを設定する」と「リレーションを構築する」という言葉では意味合いが異なるので注意してください

そのリレーションを構築するために必要な処理とは、インスタンスの OneToOneField として追加したフィールドに「リレーションを構築したいインスタンス」をセットする処理になります。

例えば、ここまで用いてきた例で考えれば、ProfileActivity には OneToOneField として user フィールドを追加していますので、Profile や Activity のインスタンスの user フィールドに対して User のインスタンスをセットする処理が必要になります。

そして、このインスタンスのセットにより、ProfileUser のインスタンス同士のリレーション、もしくは ActivityUser のインスタンス同士のリレーションが構築されることになります。

インスタンスのセットにより実際のリレーションの構築が行われる様子

ただし、セットできるインスタンスは OneToOneField の第1引数(to 引数)に指定したモデルのものだけである点に注意してください。今回は OneToOneField の第1引数に User を指定しているため、ProfileActivityuser フィールドにセットできるのは User のインスタンスのみとなります。

例えば、views.py で下記のような処理を実行すれば、User のインスタンスである user_1Profile のインスタンスである profile_X に、更に user_1Activity のインスタンスである activity_Y の間に1対1のリレーションが構築されることになります(models.pyProfileActivity が定義されている前提の例になります)。

リレーションの設定例

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

UserProfileActivity のインスタンスを生成しているのが下記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_Xuser_1 の間にリレーションが構築され、2行目で activity_Yuser_1 の間にリレーションが構築されることになります。

userフィールドにUserのインスタンスをセットすることでリレーションが構築される様子

上記では直接 User()Profile() を実行して User のインスタンスと Profile のインスタンスを生成していますが、実際には入力フォーム等でユーザーに指定された文字列や値に応じたインスタンスを生成することになると思います。

フォームの入力内容に基づいてインスタンスを生成する様子

この辺りの実際の処理については OneToOneField で拡張した User の利用例 で紹介したいと思います。

スポンサーリンク

User からの定義モデルへアクセスする

先程の処理によってインスタンス同士にリレーションが構築されたことになります。

さらに、今回は OneToOneField を利用してリレーションを設定しており、この場合、インスタンス同士で1対1のリレーションが構築されることになります。

この1体1のリレーションが構築された互いのインスタンスからは、他方のインスタンスに対してデータ属性(フィールド)からアクセスすることができるという特徴があります。

リレーションが構築された他方のインスタンスにアクセスする様子

例えば下記によって profile_Xuser_1 にリレーションを構築した場合、

profile_Xとuser_1のリレーションの構築

profile_X.user = user_1

profile_X から user_1 に対して、profile_X.user によりアクセスすることができます。これは、Profile モデルに user フィールドを持たせているので当然の話ではあります。

例えば、下記のように User モデルのフィールドである username にアクセスして表示することができます。

Profileからのusernameへのアクセス

print(profile_X.user.username)

さらに、上記のように1対1のリレーションを構築した場合、逆に user_1 から profile_X にアクセスすることもできます。具体的には、user_1.profile によってアクセスすることができます。

例えば、下記のように profile_X に設定された heightweight の値を user_1 からアクセスして表示することも可能です。

Userからのheightとweightへのアクセス

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 のインスタンスにアクセスすることができるようになり、あたかも UserProfile の持つフィールドを追加したように動作させることが可能となります。

Userのフィールドのように1対1のリレーションが構築されたモデルのフィールドが扱える様子

このように、OneToOneField を利用し、さらにインスタンス同士にリレーションを構築することで、User を変更することなく、User から新たなフィールドを追加で扱うことができるようになります。

上記の例では User から heightweight を扱うことができるようになり、User に対して heightweight のフィールドを追加した時と同様のことをアプリで実現することができるようになります。

もちろん、実際に User にフィールドを追加しているわけではないため、モデルを介してフィールドにアクセスするようなことが必要になります。この点はちょっと面倒ではあるのですが、User を変更することなく User にフィールドを追加したいような場合、OneToOneField を利用したリレーションの構築は有効だと思います。

管理画面での管理対象への定義モデルの追加

ただし、リレーションを設定していたとしても、互いのモデルは別々のモデルであるという点に注意が必要になります。

例えば、管理画面ではデフォルトで User のインスタンスの管理(追加や削除・変更など)を行うことが可能になっています。ですが、それ以外のモデルは User とリレーションが設定されていたとしてもデフォルトでは管理できないようになっています。

つまり、ここまでの例で挙げた ProfileActivity のインスタンスの管理に関してはデフォルトでは行うことができません。

そのため、ProfileActivity のインスタンスの管理を行いたい場合、これらの管理を行うことができるように管理画面の設定を行う必要があります。

そして、こういった管理画面に関する設定は admin.py で行うことができ、admin.py で管理対象に含めたいモデルを引数に指定して admin.site.regiseter を実行することで、管理画面で管理可能となるモデルの追加を行うことができます。

例えば下記のように admin.py を変更すれば、管理画面から ProfileActivity のインスタンスの管理を行うことができるようになります。

ProfileとActivityの管理

from django.contrib import admin
from .models import Profile, Activity

# Register your models here.
admin.site.register(Profile)
admin.site.register(Activity)

このように変更した場合、管理画面のトップページに ProfileActivity のリンクが追加され、

ProfileとActivityが管理画面から管理可能になった様子1

これらのリンク先から各 ProfileActivity のインスタンスの管理(変更や削除など)を行うことができるようになります。

ProfileとActivityが管理画面から管理可能になった様子2

リレーションを設定することで関係性は生まれるものの、結局は別々のモデルである点については注意が必要となります。今回は管理画面での管理対象について説明しましたが、views.py 等を実装する際にも注意が必要です。

以上が、OneToOneFieldUser を拡張する際の基本点な流れの説明となります。

OneToOneField で拡張した User の利用例

最後に、ここまでのまとめとして OneToOneField で拡張した User の利用例を紹介していきます。

今回紹介するのは OneToOneField で拡張した User を利用してユーザー登録やログインを行う例となります。

元々の未拡張の User を利用したログインについては下記ページで解説していますので、ここでは OneToOneFieldUser を拡張した時にポイントとなる点についてのみ解説を行なっていきます。

Djangoでのログイン機能の実現方法解説ページアイキャッチ【Django】ログイン機能の実現方法(関数ベースビュー編)

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 を下記のように変更します。

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)

上記によって ProfileActivity のモデルが作成され、ProfileUser の間で、さらに ActivityUser の間で1対1のリレーションが設定されます。

モデルの定義が完了したので、次は作業フォルダで下記の2つのコマンドを実行してマイグレーションを行いましょう!

% python manage.py makemigrations
% python manage.py migrate

これによって定義したモデル(今回の場合は ProfileActivity)や 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 を作成すれば良いです。

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):
    class Meta:
        model = User
        fields = ['username', 'password']

SignupFormProfileForm の2つが「ユーザー登録用入力フォーム」を実現するための Form であり、LoginForm が「ログイン用入力フォーム」を実現するための Form になります。

ポイントは、heightweight の入力を行えるようにするために ProfileForm を別途定義している点になります。UserProfile とリレーションが設定されているものの、Profile の持つ heightweight のフィールドを持っているわけではありません。

そのため、heightweight の入力を行えるようにするために、User に基づいたフォームとなる SignupForm だけでなく、ProfileForm に基づいてフォームとなる ProfileForm の定義も行っています。

MEMO

補足しておくと、今回は UserCreationFormModelFormを継承してモデル毎に 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 のフォルダの中に作成する必要があります。

signup.html

{% 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>

login.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>

logout.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>

user.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>

other.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 の持つフィールドである heightweight の入力が受け付けられなくなるので注意してください。

2つ目のポイントは user.htmlother.html における User のインスタンスの情報表示になります。

特に分かりやすいのが user.html で、user.html では views.py から user として User のインスタンスを受け取り、user にリレーションが設定された ProfileActivity のインスタンスに user からアクセスしています。

このアクセスは、これらのモデル名を小文字にした profileactivityuser のデータ属性として指定することで実現しています。

さらに、これらのインスタンスを介して、user.profile.heightuser.profile.weight によって user の身長と体重、さらには、user.activity.login_num によって user のログイン情報を表示するようにしています。

また、other.html はログインユーザー以外のユーザーの情報を「ログイン回数」に対して降順に並べて表示するためのテンプレートとなっています。

views.py からは「ログイン回数に対して降順に並べた Activity のインスタンス」のクエリーセットである activities を受け取ることを想定しているテンプレートであり、for 文で activities から Activity のインスタンスを activity として1つずつ抽出し、activity からユーザー名やログイン回数、ユーザーの身長と体重を表示するようにしています。

ユーザー名に関しては User モデルの持つフィールドであり(username フィールド)、さらに ActivityUser の間にはリレーションが設定されています。そのため、Userusername には Activity のインスタンスである activity  からアクセスすることができます。

具体的には、activity.user.usernameactivity とリレーションが構築されている User のインスタンスのユーザー名となります。なので、ユーザー名の表示は activity.user.username の表示により実現できます。

また、身長と体重に関しては Profile の持つフィールドになります。ただし、ActivityProfile の間ではリレーションの設定が行われていないため、Activity から Profile に直接アクセスすることはできません。

ActivityからProfileに直接アクセスできない様子

ですが、ActivityUser の間、更に UserProfile の間ではリレーションが設定されているため、Activity のインスタンスから User のインスタンスを介して Profile のインスタンスにアクセスすることができます。したがって、activity とリレーションが構築されている User のインスタンスの身長と体重の表示は、activity.user.profile.heightactivity.user.profile.weight の表示により実現することができます。

このように、Activity から Profile の間にリレーションは設定されていませんが、両者とリレーションが設定されている User を経由することで、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 の中身は下記のようになります。

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 である時に、送信されてきたフォームに基づいて UserProfile のインスタンスの生成を行なっています。

この辺りの処理を行なっているのが signup_view の下記部分になります。

UserとProfileのインスタンス生成と保存

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) を実行することで、送信されてきたフォームに基づいて SignupFormProfileForm のインスタンスが生成されます。

これらはモデルではなくフォームですが、これらのフォームは ModelForm を継承するフォームであり、ModelForm を継承するフォームのインスタンス生成時に引数 request.POST を指定してやることで、送信されてきたフォームの情報に基づいたモデルのインスタンスの生成も行われることになります。

具体的には、ModelForm はフォーム定義時のクラス変数 model に指定したモデルに基づいたフォームを生成するクラスであり、ModelForm を継承するフォームのインスタンスを生成する際(コンストラクタを実行する際)には、その model に指定されたモデルのインスタンスも生成されることになります。

ModelFormのインスタンス生成時にモデルのインスタンスも生成される様子

MEMO

models.py で SignupFormModelForm ではなく UserCreationForm を継承させる形で定義しており、ModelForm は関係ないようにも思えるかもしれません

ですが、UserCreationFormModelForm を継承するフォームなので、ModelForm と同じ特徴を持っており、前述のようにフォームのインスタンスを生成する際に、その model に指定されたモデルのインスタンスも生成されることになります

ただし、SignupForm(request.POST)ProfileForm(request.POST) の返却値はフォームのインスタンスであり、モデルのインスタンスではありません。

フォームからのモデルのインスタンスの取得は save メソッドにより行うことができます。

つまり、下記部分でフォームのインスタンスに save メソッドを実行させることで、返却値としてモデルのインスタンス(User のインスタンスと Profile のインスタンス)の取得を行なっています。

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 フィールドには何もセットしていないため、このままデータベースへの保存をしようとするとエラーとなります。

Profileのインスタンスのuserに何も設定されていない様子

そのため、わざわざ commit=False を指定して Profile のインスタンスを取得するために save メソッドを実行し、取得した Profile のインスタンスに下記で user フィールドの設定を行ってから、再度 commit=False を指定せずに save メソッドを実行してデータベースへの保存を行うようにしています。

このように、OneToOneField を必須フィールドとなっている場合、リレーションを構築してからでないとデータベースへの保存時にエラーになるので注意してください。

リレーションの構築と保存(Profile)

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()
activity.user = user

login(request, user)

user.activity.login_num += 1
user.activity.save()

# 下記でもOK
# activity.login_num += 1
# activity.save()

コメントアウトをしていますが、activity.user = useractivityuser の間に1対1のリレーションが構築されているため、”user の” login_num は user.activity.login_num からだけでなく、activity.login_num からもアクセスすることが可能です。

また、上記は signup_view での処理になりますが、ログインに関しては login_view でも行われるため、login_view でも同様の処理を行なっています。

あとは、通常のログイン時やログイン中のユーザーの情報を表示する時と同様の処理になると思いますので、分からない点があれば下記ページを参照していただければと思います。

Djangoでのログイン機能の実現方法解説ページアイキャッチ【Django】ログイン機能の実現方法(関数ベースビュー編)

その他のファイルの変更・作成

ここまでの変更やファイルの作成によって、Django における MVT の部分が完成したことになります。

あとは残りの必要なファイルの変更・作成について紹介していきます。

まず、URL と View の関連付けを行うため、login_app/urls.py を下記のように新規作成します。

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 を次のように変更します。

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')),
]

また、管理画面から ProfileActivity のインスタンスが管理できるようにするため、管理画面での管理対象への定義モデルの追加 での解説に倣って login_app/admin.py を下記のように変更します。

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 の一番最後の行に下記を追記します。

settings.py

LOGIN_URL = '/login_app/login/'

以上が、OneToOneField によって拡張した User によるログインを実現するスクリプトの例となります。

ほぼユーザー管理(登録やログイン・ログアウトなど)を行うだけのスクリプトですが、OneToOneField によって拡張した User の利用例としては分かりやすいのではないかと思います。

動作自体は下記ページの最後に紹介しているスクリプトと同様になりますので、動作確認したい場合は下記ページを参考にしていただければと思います(next の利用に関しては上記のスクリプトでは省略させていただいています)。

Djangoでのログイン機能の実現方法解説ページアイキャッチ【Django】ログイン機能の実現方法(関数ベースビュー編)

スポンサーリンク

まとめ

このページでは、Django における「OneToOneField を利用した User の拡張」について解説しました。

OneToOneField を利用することでモデル間に1対1のリレーションを設定することができます。また、リレーションを設定した上で各モデルのインスタンス間で実際にリレーションを構築してやれば、一方のインスタンスから他方のインスタンスを参照することができるようになります。

そして、これらを利用することで、User を変更しなくても User から他のモデルのフィールドを参照することができ、User から新たなフィールドを扱うことができるようになります。

今回は User を拡張することを目的とした解説になりましたが、もちろん他のモデルを変更することなく拡張したいような場合にも利用できます。

また、User の拡張にはカスタムユーザーを別途定義する方法もあります。これに関しては下記ページで解説していますので、興味があれば是非読んでみてください!

【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractUser編】

カスタムユーザーをAbstractBaseUserを継承して作成する手順の解説ページアイキャッチ【Django】カスタムユーザー(独自のユーザー)の作り方【AbstractBaseUser編】

同じカテゴリのページ一覧を表示