【Django入門8】モデルフォームの基本

Djangoのモデルフォーム解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Django におけるモデルフォームについて解説していきます!

このサイトでは、下記ページで Django のモデルの基本について、

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

下記ページでは Django のフォームの基本について解説しています。

Djangoのフォームの解説ページアイキャッチ 【Django入門5】フォームの基本

モデルフォームとは、名前の通り『モデル』に基づいた『フォーム』のことになります。これを利用することで、フォームクラスの定義を効率的に行うことができます。

このページでは、まずモデルフォームの基本的な事柄について解説を行い、その後モデルフォームの扱い方について解説していきたいと思います。

モデルフォームの基本

まず、モデルフォームの基本的な事柄について解説していきたいと思います!

モデルとフォーム

モデルとは、下記ページでも解説しているように、データベースの管理を役割とするモジュールです。

モデルの解説ページアイキャッチ 【Django入門6】モデルの基本

モデルを利用することで、データベースにデータを保存したり、データベースからデータを取得したりすることができるようになります。

モデルの説明図

それに対し、フォームとは、下記ページでも解説しているようにフォームの管理を役割とするモジュールとなります。

Djangoのフォームの解説ページアイキャッチ 【Django入門5】フォームの基本

フォームを利用することで、ユーザーはウェブアプリに対してデータの送信を行うことができるようになります。

フォームの説明図

フォームによって送信されてきたデータの使い道はウェブアプリによって様々ですが、その使い道の1つとして『送信されてきたデータをデータベースに保存する』が挙げられます。

フォームから送信されてきたデータをデータベースに保存する様子

例えば掲示板アプリでの『コメント投稿フォーム』の例で考えると、このフォームから送信されてきたコメントのデータは一旦データベースに保存されることになります。そして、次にユーザーが掲示板を表示したときにデータベースに保存されているコメントを取得して表示することで、保存されたコメントを後から表示することができるようになります。

このように、フォームから送信されてきたデータは一旦データベースに保存し、後から利用するという使い方が多いです。

スポンサーリンク

モデルフォーム

この時にデータベースに保存するデータの多くは、フォームでユーザーがフィールドに入力したデータとなります。そして、入力受付を行うフィールドの種類や数はフォームクラスの定義によって決まります

フォームクラスと表示されるフォームのフィールドの関係性

また、データベースに保存するデータの種類や数はテーブルの持つフィールド(カラム)によって決まります。そして、テーブルの持つフィールドの種類や数はモデルクラスの定義によって決まります(実際にはプライマリーキーとなる id フィールドも存在しますが、図では省略しています)。

モデルクラスとテーブルのフィールドの関係性

更に、フィールドに入力されたデータを全てデータベースに保存することを考えれば、『フォームのフィールドの種類や数』は『テーブルのフィールドの種や数類』と同様のものである必要があります。つまり、この場合、フォームクラスで定義するフィールドと、モデルクラスで定義するフィールドは同様のものである必要があることになります。

フォームから送信されてきたデータをデータベースに保存する際のフォームクラスとモデルクラスの関係性

同様のフィールドを定義する必要があるのであれば、フォームクラスとモデルクラスを別々に定義するよりも、一方のクラスから他方のクラスを自動的に作成する方が楽ですよね。同じようなフィールドの定義を個別に行うのは効率が悪いです。

この「一方のクラスから他方のクラスを自動的に作成する」ことを実現するのがモデルフォームとなります。

このモデルフォームを利用することで、モデルクラスの定義からフォームクラスを自動的に定義することが可能となります。つまり、モデルクラスさえ定義してやれば、このモデルクラスの持つフィールドと同様のフィールドを持つフォームクラスを簡単に定義できるようになります。

モデルフォームの説明図

ここではモデルクラスとフォームクラスとが同様のフィールドを持つことを想定した説明を行なっていますが、あくまでもモデルフォームはモデルクラスをベースとしてフォームを定義する仕組みであり、フォームクラスに持たせるフィールドはモデルクラスの持つフィールドの中から必要なもののみ選択することもできますし、逆にモデルクラスの持たないフィールドをフォームに新たなフィールドとして追加するようなことも可能です。

モデルフォームクラスの定義

通常のフォームと同様に、モデルフォームに関してもクラスを定義して利用することになります。このクラスのことを、以降ではモデルフォームクラスを呼ばせていただきます。

次は、このモデルフォームクラスの定義方法について解説していきます。このモデルフォームクラスはフォームクラスの一種であり、フォームクラス同様に forms.py に定義を行います。

ModelForm を継承する

通常のフォームクラスを定義する場合は Form というクラスを継承する必要がありましたが、モデルフォームクラスを定義する場合は ModelForm というクラス(もしくは ModelForm のサブクラス)を継承する必要があります。

ModelFormの継承
from django import forms

class クラス名(forms.ModelForm):

model を指定する

更に、ベースとするモデルを決めるため、モデルフォームクラスを定義する場合は model 属性を指定する必要があります。この model 属性は下記のように class Meta: ブロックの中で指定する必要があり、model には モデルクラス を指定します。

modelの指定
class クラス名(forms.ModelForm):
    class Meta:
        model = モデルクラス

model に指定するモデルクラスは事前に import しておく必要がある点に注意してください。

fields or exclude を指定する

model 属性の指定によって、モデルフォームクラスの基となるモデルクラスが設定されたことになります。

次は、このモデルフォームクラスで扱いたいフィールドを、指定したモデルクラスのフィールドの中から選択します。

この選択は fields 属性もしくは exclude 属性の指定によって実現できます。この fields 属性 or exclude 属性に関しても、class Meta: ブロックの中で指定を行う必要があります。

fields 属性を指定する場合は、モデルフォームクラスで “扱いたい” フィールドを指定します。基本的にはリストやタプル形式で指定を行い、各要素にはモデルクラスの持つフィールドのフィールド名(クラス変数名)を文字列で指定します。

fieldsの指定
class クラス名(forms.ModelForm):
    class Meta:
        model = モデルクラス
        fileds = ['フィールド名1', 'フィールド名2',....]

モデルクラスの持つ全てのフィールドをモデルフォームクラスで扱いたい場合は、リストやタプルではなく文字列で '__all__' を指定します。

また、exclude 属性を指定する場合は、モデルフォームクラスで “扱いたくない” フィールドをリストやタプル形式で指定します。各要素にはモデルクラスの持つフィールドのフィールド名(クラス変数名)を文字列で指定します。空のリストを指定した場合は、モデルクラスの持つ全てのフィールドを扱うことになります。

excludeの指定
class クラス名(forms.ModelForm):
    class Meta:
        model = モデルクラス
        exclude = ['フィールド名1', 'フィールド名2',....]

fileds = '__all__'exclude を指定した場合、モデルクラス側へのフィールドの追加がモデルフォームクラス側に自動的に反映されることになり、モデルクラスの変更に強いフォームを実現することができます。

そのため、fileds に手動で1つずつフィールド名を指定するよりは、fileds = '__all__'exclude の指定を積極的に利用することをオススメします。

新たなフィールドを追加する

ここまで説明した ModelFormの継承、model の指定、fields or exclude の指定に関しては、モデルフォームクラスを定義する際に必須の手順となります。

それに対し、ここから説明する手順に関しては必須ではなく、必要に応じて行えば良い手順となります。

前述の通り、fields 属性や exclude 属性の指定によって、model に指定したモデルクラスの持つフィールドをモデルフォームクラスに持たせることができます。ただし、model に指定したモデルクラスの持つフィールド以外のフィールドをフォームに持たせたいような場合もあると思います。

その場合は、下記のように通常のフォームの時と同様に Field のサブクラスを利用してフィールドを追加してやれば良いです。

フィールドの追加
class クラス名(forms.ModelForm):
    フィールド名 = forms.Fieldのサブクラス()
    class Meta:
        model = モデルクラス
        exclude = ['フィールド名1', 'フィールド名2',....]

各種フィールドをカスタマイズする

また、モデルフォームクラスに定義した各種フィールドをカスタマイズするようなことも可能です。

例えば、class Meta のブロックの中に labels を辞書形式で指定してやれば、各種フィールドのラベル名を変更することが可能です。labels に指定する辞書では、キー にモデルフォームクラスの持つフィールド名、 にそのフィールドのラベル名を指定します。

他にも、help_texts を指定して各種フィールドの説明文を設定したり、widgets を指定して各種フィールドのウィジェットを設定するようなことも可能です。これらに関しても、辞書形式で指定を行う必要があります。

例えば、モデルフォームクラスの基になるモデルに usernameemailself_introduction が存在する場合、下記のようにモデルフォームクラスを定義すれば各種フィールドのラベル名を変更し、さらに self_introduction フィールドのウィジェットを Textarea(テキスト入力向けのウィジェット)に設定することができます。

labelsの指定例
from django import forms
from .models import User

class UserForm(forms.ModelForm):

    class Meta:
        model = User
        fields = '__all__'
        labels = {
            'username': 'ユーザー名',
            'email': 'メールアドレス',
            'self_introduction': '自己紹介文'
        }

        widgets = {
            'self_introduction': forms.Textarea
        }

実際に、上記によってカスタマイズした結果の各種フィールドは下の図のようになります。

カスタマイズ後のフィールド

モデルフォームクラスの定義例

次は、具体例でフォームクラスの定義の仕方について解説していきます。

今回は、下記のようなモデルクラスと同様のフィールドを持つフォームを例として、モデルフォームクラスの定義の仕方について説明していきます。

モデルクラスの例
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=30)
    email = models.EmailField()
    age = models.IntegerField()
    height = models.FloatField()
    weight = models.FloatField()

通常のフォームクラスでの定義例

通常のフォームクラスの場合、つまり Form を継承するクラスの場合、モデルとの関連性がないため別途必要なフィールドをフォームクラスに定義する必要があります。

したがって、上記の User クラスと同様のフィールドを持つフォームクラスは下記のようにして定義する必要があります。

フォームクラスの例
from django import forms

class UserForm(forms.Form):
    name = forms.CharField(max_length=30)
    email = forms.EmailField()
    age = forms.IntegerField()
    height = forms.FloatField()
    weight = forms.FloatField()

モデルクラス側と同様のフィールドを持たせるために、フォームクラス側でも同様のフィールドの定義を行う必要があって実装量が多くなります。また、モデルクラス側のフィールドを変更してしまうとフォームクラス側の変更も必要となり、メンテナンス性も悪いです。

モデルフォームクラスでの定義例

それに対し、モデルフォームクラスを利用する場合、つまり ModelForm を継承するクラスを利用する場合、モデルクラスに基づいてクラスを定義することができ、モデルクラスの持つフィールドを自動的にフォーム側に持たせることができます。

上記の User クラスと同様のフィールドを持つフォームクラスは下記のようにして定義することができます。exclude = [] の部分は fields = '__all__' にしても同様のフォームクラスが得られます。

フォームクラスの例
from django import forms

class UserModelForm(forms.ModelForm):
    class Meta:
        model = User
        exclude = []

model に指定したモデルクラスに応じてフィールドが自動的に設定されることになるため実装量も減りますし、モデルクラスの変更も自動的に反映されるためメンテナンス性も良くなります。

スポンサーリンク

モデルフォームクラスでフォームを扱う

モデルフォームクラスの定義方法については理解していただけたでしょうか?

次は、モデルフォームクラスの扱いについてのポイントを解説していきます。フォームクラスの時同様、モデルフォームクラスを扱うのはビューとなります(テンプレートから参照することも可能)。

次は、モデルフォームクラスのビューからの扱いについて解説していきたいと思います。

基本的な扱い方はフォームクラスと同じ

まず、フォームクラスもモデルフォームクラスも両方とも BaseForm というクラスのサブクラスとなります。同じクラスを継承しており、基本的な扱い方はフォームクラスの場合もモデルフォームクラスの場合も同様になります。

例えば、これらのクラスのインスタンスはコンストラクタを引数なしで実行することでも生成することができますし、コンストラクタの引数に request.POST を指定して実行することで生成することもできます。後者の場合は、フォームから送信されてきたデータが各種フィールドにセットされた状態のインスタンスを生成することができます。

また、is_valid メソッドでフォームから送信されてきたデータの妥当性の検証を行うことができる点も、通常のフォームクラスと同様になります。ただ、これに関しては少し注意事項がありますので、後述の 妥当性の検証はモデルクラスに基づいて実行される で詳細を解説します。

save メソッドでデータベースへの保存が可能

ここからは、通常のフォームクラスには無い、モデルフォームクラスならではの機能について解説していきます。その機能の1つがデータベースへの保存になります。

モデルフォームクラスには save メソッドが用意されており、モデルフォームクラスのインスタンスに save メソッドを実行させることで、フォームの各種フィールドにセットされているデータをレコードとしてデータベースのテーブルに直接保存することができます。

保存先はクラスの定義時に model に指定したモデルクラスに対応するテーブルとなります。 

通常のフォームクラスには save メソッドは用意されておらず、フォームから送信されてきたデータをデータベースに保存するためには、各種フィールドのデータを取得してモデルのインスタンスにセットし、さらにモデルのインスタンスに save メソッドを実行させる必要があります。

フォームクラスの各種フィールドにセットされたデータをデータベースに保存する様子

また、各種フィールドのデータを取得する際に妥当性の検証を行うことを考えると、request.POST をコンストラクの引数に指定してフォームクラスのインスタンスを生成し、is_valid メソッド実行後にインスタンスの cleaned_data からデータを取得する必要もあります。

それに対し、モデルフォームクラスの場合は、request.POST をコンストラクの引数に指定してインスタンスを生成し、そのインスタンスに is_valid メソッドおよび save メソッドを実行させるだけでデータベースへの保存を実現することが可能となります。つまり、わざわざ各種フィールドのデータをモデルクラスのインスタンスにセットする必要がなくなります

モデルフォームクラスのインスタンスの各種フィールドにセットされたデータをデータベースに保存する様子

補足しておくと、フォーム自体はデータベース管理の役割を持ちません。これはモデルフォームクラスの場合も同様です。そのため、モデルフォームクラスのインスタンスに save メソッドを実行させた際には、その save メソッドの中で結局はモデルクラスの save メソッドが実行されることになります。

なので、内部的な処理も考慮すれば、モデルフォームクラスの場合も通常のフォームクラスの場合も結局は同じような処理が行われることになります。ですが、ビューへの実装を考えると、モデルフォームクラスのインスタンスからの save メソッド実行のみでデータベースへの保存が行えるため実装量は減り、より簡潔な処理でデータベースへの保存を実現することができることになります。

スポンサーリンク

モデルクラスのインスタンスが取得可能

また、モデルフォームクラスのインスタンスからモデルクラスのインスタンスを取得することも可能です。

モデルフォームクラスのインスタンスからモデルクラスのインスタンスを取得する様子

ここではインスタンスの取得方法として2つの方法を取得します。

save メソッドの返却値として取得する

1つ目は save メソッドの返却値として取得する方法になります。save メソッドを実行した際には、返却値として model に指定したモデルクラスのインスタンスが得られます。モデルフォームクラスのインスタンスの各種フィールドにデータがセットされている場合は、それらのデータが各種フィールドにセットされた状態のモデルクラスのインスタンスが返却されることになります。

例えば下記は、モデルフォームクラスの定義例 で示した UserModelForm のインスタンスに save メソッドを実行させ、その返却値として モデルフォームクラスの定義例 で示した User のインスタンスを取得する例になります。

saveメソッドからのモデルの取得
from .forms import UserForm
from .models import User

def form(request):
    if request.method == 'POST':
        form = UserModelForm(request.POST)
        if form.is_valid():
            user = form.save() # Userのインスタンスを取得

前述の通り、モデルフォームクラスのインスタンスに save メソッドを実行させた場合はデータベースへの保存が行われます。逆に言えば、上記の方法ではデータベースへの保存を行わないとモデルフォームクラスのインスタンスが取得できないことになります。

ですが、データベースの保存を行う前にモデルフォームクラスのインスタンスを取得したいようなケースも多々あります。そのような場合は、save メソッドの引数に commit=False を指定します。これにより、save メソッド実行時のデータベースへの保存がスキップされ、データベースへの保存なしにモデルクラスのインスタンスが取得できるようになります。

例えば、モデルフォームクラスの定義例 で示した User に必須項目として bmi フィールドを追加したとしましょう。さらに UserModelForm には bmi フィールドを持たせないものとします。つまり、bmi フィールドはユーザーから入力受付するのではなく、アプリ内で自動的に計算してフィールドにセットする必要があります。

MEMO

BMI は身長と体重から算出される、肥満度を表す指標値となります

この場合、bmi フィールドをセットする前にインスタンスを取得し、さらにそのインスタンスに bmi フィールドをセットしてからインスタンスの保存を行う必要があります。このような流れの処理は下記のように実装することで実現することができます。

saveメソッドからのモデルの取得2
from .forms import UserForm
from .models import User

def form(request):
    if request.method == 'POST':
        form = UserModelForm(request.POST)
        if form.is_valid():
            user = form.save(commit=False) # Userのインスタンスを取得
            bmi = user.weight / ((user.height /100) * (user.height / 100))
            user.bmi = bmi # モデルクラスのインスタンスにセット
            user.save() # モデルクラスからデータベースに保存

上記において、bmi フィールドは必須項目である(null=True が指定されていない)、かつ、UserModelFormbmi フィールドの入力受付は行なっていないため、form.save(commit=False) の箇所を form.save() に変更すると、save メソッド実行時に例外が発生することになります。これは、必須項目のフィールドにデータが設定されていない状態でデータベースへのレコード保存が行われることになるためです。

こういった、必須項目がまだ未設定の場合にモデルフォームから save メソッドでインスタンスを取得したいような場合は、データベースへのレコード保存をスキップするよう commit=False を指定しておく必要があります。

データ属性 instance から取得する

2つ目はモデルフォームクラスのインスタンスのデータ属性 instance から取得する方法になります。

通常のフォームクラスの場合は is_valid メソッドの実行によってインスタンスにデータ属性 cleaned_data が追加されますが、このデータは辞書形式のデータでした。モデルフォームクラスの場合は is_valid メソッドの実行によって、インスタンスにデータ属性 cleaned_data だけでなくデータ属性 instance も追加されることになります。

この instancemodel に指定したモデルクラスのインスタンスであり、このデータ属性から直接モデルクラスのインスタンスを取得することができます。この場合も、データベースへの保存を行う前にモデルクラスのインスタンスを取得することが可能です。

instanceからのモデルの取得
from .forms import UserForm
from .models import User

def form(request):
    if request.method == 'POST':
        form = UserModelForm(request.POST)
        if form.is_valid():
            user = form.instance # Userのインスタンスを取得

モデルからモデルフォームのインスタンスが生成可能

また、モデルフォームクラスのインスタンスは、モデルクラスのインスタンスから生成することも可能です。

モデルフォームクラスのコンストラクタでは引数に instance を指定することが可能です。この instance にモデルクラスのインスタンスを指定した場合、そのインスタンスからモデルフォームクラスのインスタンスを生成することができます。この生成されるインスタンスの各種フィールドには、モデルクラスのインスタンスの各種フィールドにセットされたデータがセットされることになります。

モデルクラスのインスタンスからモデルフォームクラスのインスタンスを生成する様子

このようなインスタンスの生成の仕方は、既にデータベースに保存されているレコードの内容を反映させた状態のフォームを表示したい場合に有効です。

モデルクラスのインスタンスの各種フィールドの情報を反映したフォームを表示する様子

レコードの更新

また、このようなインスタンスの生成の仕方はデータベースのレコードを更新する際にも便利です。

例えば、あるユーザーが自身の登録情報を変更したい場合、まずはそのユーザーの登録情報(レコード)をフィールドに反映したフォームを表示してあげるのが親切です。

前述の通り、レコードの各種フィールドのデータを反映したフォームは、モデルクラスのインスタンスからモデルフォームクラスのインスタンスを生成することで簡単に実現することができます。

例えばですが、ユーザーの登録情報をフィールドに反映したフォームの表示は下記のような views.py の update 関数によって実現することができます。app/update.html はフォームを表示するためのテンプレートファイルであることを想定しています。また、ユーザーを表すモデルクラスを User とし、さらに登録情報を変更しようとしているユーザーの id は引数 id で渡されるものとして関数を定義しています。

更新ページの表示
from django.shortcuts import render
from .forms import UserModelForm
from .models import User

def update(request, id):
    user = User.objects.get(id=id) #Userのインスタンスを取得
    if request.method != 'POST':
        form = UserModelForm(instance=user)

    context = {
        'form' : form
    }
    return render(request, 'app/update.html', context)

上記では、ユーザーの登録情報をフィールドに反映したフォームの表示を実現しようとしているだけなので、'GET' メソッドのリクエストを受け取った場合の処理のみを実装しています。

UserModelForm() の引数に instance=user を指定しているため、user の各種フィールドにセットされたデータが、生成される UserModelForm のインスタンスの各種フィールドにセットされることになります。したがって、このインスタンスのフォームをページに埋め込んで表示すれば、各種フィールドに user の情報が入力された状態のフォームが表示されることになります。 

次は、上記の update 関数を変更してレコードの更新が行えるようにしていきたいと思います。具体的には、表示されるフォームの各種フィールドにユーザーが更新したいデータを入力し、さらにフォームから更新後のデータが送信されてきた際に、その送信されてきたデータで先ほどデータベースから取得したレコードを上書きするような処理を実現していきます。

フォームから送信されてきたデータでデータベースを更新する様子

このようなレコードの上書きは、モデルフォームクラスのコンストラクタに request.POSTinstance=上書き先のレコードに対応するインスタンス を指定し、これによって生成されるモデルフォームクラスのインスタンスに save メソッドを実行させることで実現することができます。

先程の update 関数の例で考えれば、'POST' メソッドのリクエストを受け取った際(下記の else 節)に下記のような処理を行えば良いことになります。

更新ページの表示
from django.shortcuts import render
from .forms import UserModelForm
from .models import User

def update(request, id):
    user = User.objects.get(id=id) #Userのインスタンスを取得
    if request.method != 'POST':
        form = UserModelForm(instance=user)

    else:
        form = UserModelForm(request.POST, instance=user)
        if form.is_valid():
            form.save()
            context = {
                'form' : form,
            }
            return render(request, 'app/update.html', context)

    context = {
        'form' : form
    }
    return render(request, 'app/update.html', context)

コンストラクタに指定した request.POST はフォームから送信されてきたデータになります。コンストラクタに request.POST のみを引数に指定してインスタンスを生成し、このインスタンスに save メソッドを実行させた場合はデータベースへのレコード新規追加による保存処理が行われることになります。

レコードが新規追加される様子

それに対し、コンストラクタに対して instance を追加で引数指定した場合、instance によって上書き先のレコードが指定されることになるため、生成されたモデルフォームクラスのインスタンスに save メソッドを実行させれば、レコードが新規追加されるのではなく instance 引数に指定したインスタンスに対応するレコードを上書きする形で保存処理が行われることになります。

レコードが更新される様子

このように、モデルフォームクラスのコンストラクタに指定する引数によって、save メソッド実行時の動作が異なることになるので注意してください。

ここまで説明してきたように、モデルフォームクラスは通常のフォームクラスと同様の機能を利用するのに加え、データベースの管理も行うことができるクラスとなります。

フォームから送信されてきたデータをデータベースに保存したい場合や、データベースに保存されているデータをフォームに反映したい場合などはモデルフォームクラスを使った方が簡潔に処理が記述できると思います。

ただし、フォームから送信されてきたデータをデータベースに保存しないような場合は、通常のフォームクラスを利用することになります。モデルフォームクラスと通常のフォームクラスの違いを理解し、適切に使い分けるようにしましょう!

妥当性の検証はモデルクラスに基づいて実行される

解説の最後に、モデルフォームにおける妥当性の検証について解説しておきます。

ここまで示してきたコードでも既に使用例を示していますが、通常のフォームクラスと同様に、モデルフォームクラスのインスタンスからも is_valid メソッドを実行することができます。そして、このメソッドの実行により、フォームの各種フィールドに入力されたデータの妥当性の検証を行うことができます。

妥当性の検証の設定は基本的にはモデルクラスで行う

ただし、通常のフォームクラスの場合、妥当性の検証時にはフォームクラスの各種フィールドに基づいて妥当であるかどうかの判断が行われていました。それに対し、モデルフォームクラスの場合、この判断は基本的にはモデルクラスの各種フィールドに基づいて行われることになります。

例えば、通常のフォームクラスの場合、フォームクラスの各種フィールドにおける Field のサブクラスの種類に応じた妥当性の検証が行われていました。Field のサブクラスが  IntegerField の場合は整数以外が入力された際に妥当でないと判断されるといった感じです。

モデルフォームクラスの場合、前述の通りモデルクラスの各種フィールドに基づいて妥当性の検証が行われることになります。例えば先程の例で考えれば、フォームクラスではなくモデルクラスの各種フィールドにおける Field のサブクラスの種類によって妥当性の検証が行われることになります。

また、Field のサブクラスの種類に応じてクライアント側(ウェブブラウザ)でも妥当性の検証が行われる点もフォームクラスと同様の点になります。

さらに、フォームクラス同様にモデルクラスにバリデーターをセットすることもでき、これによってアプリ独自の判断基準で妥当性の検証が行われるようにすることも可能です。

このように、モデルフォームを利用する場合、妥当性の検証は、基本的にはモデルクラスに基づいて行われます。モデルクラスのフィールドの Field のサブクラスの種類に応じて妥当性の検証が行われますし、より細かな妥当性の検証を行いたいのであれば、モデルクラスのフィールドに対してバリデーターをセット指定やれば良いです。要は、フォームクラスのフィールドで行なっていた妥当性の検証に関する設定を、モデルクラス側に移行してやれば良いです。

フィールドに指定可能な引数の違いに注意

ただ、1つ厄介な点があって、それはフォームクラスとモデルクラスとで Field のサブクラスのコンストラクタに指定可能な引数が異なるという点になります。

例えば、forms.IntegerField に関しては引数として min_valuemax_value が指定可能であり、これらの引数によってフィールドに入力可能な最小値と最大値を設定することができます。そして、このフィールドを持つフォームをテンプレートファイルに埋め込めば、この引数に応じてフィールド要素のタグの属性が設定されることになります(この場合は minmax 属性が自動的に付加される)。

そして、このタグの属性によってクライアント側(ウェブブラウザ)で妥当性の検証が行われることになります。さらに、この引数に応じてサーバー側でも妥当性の検証が行われることになります。要は、IntegerField() の引数の指定によって妥当性の検証時の判断基準を決めることができました。

それに対し、models.IntegerField の場合は引数として min_value max_value は指定不可です。そのため、forms.IntegerField の時のような引数指定による最小値と最大値に対する妥当性の検証は実現できないことになります。もちろん、バリデーターを別途用意して最小値と最大値を定義し、それ以外の値が入力された際に妥当でないと判断されるようにしてしまえば良いのですが、これだとクライアント側での妥当性の検証が実施されません。これは、バリデーターを指定したとしても HTML のフィールド要素のタグ属性への反映が行われないためです。

そのため、フィールド要素のタグ属性の設定を行うためには、それ専用の処理を行う必要が出てきます。具体的には、モデルフォームクラスにコンストラクタを用意し、そのコンストラクタの中でタグ属性の設定を行います。

具体例を考えていきましょう!

例えば、フォームクラスを利用して下記のようなフォームを定義したとします。

フォームクラスを利用した最小値と最大値
from django import forms

class UserForm(forms.ModelForm):
    age = forms.IntegerField(min_value=0, max_value=200)

このフォームクラスのインスタンスをテンプレートに埋めんで HTML を生成すれば、クライアント側のウェブブラウザでも入力値の妥当性の検証が行われ、引数で指定した min_valuemax_value の範囲外の値が入力された場合は下の図のような警告文が表示されることになります。

クライアント側での妥当性の検証により妥当でないと判断された際の注意文

さらに、クライアント側で妥当性の検証が行われなかった場合、引数で指定した min_valuemax_value の範囲外の値がサーバー側に送信されてくることになります。その場合もサーバー側で min_valuemax_value に基づいて妥当性の検証が行われ、これらの範囲外の値が送信されてきた際にはデータが妥当でないと判断され、再度フォームを表示した際に、そのことを示す注意文を表示するようなことも可能です。

サーバー側での妥当性の検証により妥当でないと判断された際の注意文

これと同様のフォームをモデルクラスフォームを利用して実現する際には、次のような手順を踏む必要があります。

まずはモデルクラスの定義を行います。前述の通り、モデルフォームクラスにおいてはモデルクラスに基づいて妥当性の検証が行われることになりますが、これも前述したとおり models.IntegerField への min_valuemax_value の指定は不可です。そのため、下記のように MinValueValidator と MaxValueValidator を利用してサーバー側での妥当性の検証へ最小値と最大値に対する判断基準追加します。

モデルクラスを利用した最小値と最大値
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator

min_age = 0
max_age = 200

class User(models.Model):
    age = models.IntegerField(validators=[MinValueValidator(min_age), MaxValueValidator(max_age)])

続いて、下記のようにモデルクラスフォームを定義します。下記ではモデルクラスフォームのコンストラクタを定義し、このコンストラクタの中で、age フィールドのタグ属性の設定を行うようにしています。これにより、テンプレートファイルにこのフォームを埋め込んだ際、HTML の age フィールド要素に対し、ここで設定したタグ属性が自動的に付加されるようになります。

モデルフォームクラスを利用した最小値と最大値
from django import forms
from .models import User, max_age, min_age

class UeerForm(forms.ModelForm):

    class Meta:
        model = User
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['age'].widget.attrs.update({
            'min': min_age,
            'max': max_age,
        })

上記のようにモデルクラスとモデルフォームクラスを定義すれば、前述で示したフォームクラスと同等の妥当性の検証を実現することができることになります。

特にモデルフォームクラスのコンストラクタを作成する必要があるあたりが、正直ちょっと面倒ですよね…。ただ、min_valuemax_value のようなフォームのフィールドでのみ指定可能な引数は少ないと思いますし、妥当性の検証で特に重要になるのはサーバー側で行われる検証になります。これは、下記ページでも解説していますがクライアント側での妥当性の検証が行われるとは限らないためです。結局サーバー側での妥当性の検証が必要になります。

Djangoのフォームの解説ページアイキャッチ 【Django入門5】フォームの基本

ですので、上記のようにモデルフォームクラスのコンストラクタを作成してタグの属性の設定を行うようなことは毎回行わなくても良いと思いますし、上記のコンストラクタで行っている処理内容もしっかり理解しておく必要はないとは思っています。ですが、上記のようにコンストラクタを設けてタグの設定を行うことができること自体は頭の片隅に置いておくと良いと思います。

また、別に HTML のタグの属性によって妥当性の検証を行わなくても、JavaScript を利用して各フィールドに対して妥当性の検証を行うようなことも可能です。このことも是非覚えておいてください。

スポンサーリンク

掲示板アプリでモデルフォームを利用してみる

では、いつも通り、ここまで説明してきた内容を踏まえて、実際にモデルフォームの利用例を示していきたいと思います。

この Django 入門に関しては連載形式となっており、ここでは前回下記ページの 掲示板アプリでリレーションを利用してみる で作成したウェブアプリに対してモデルフォームを導入する形で、モデルフォームの利用例を示していきたいと思います。

Django のリレーションについての解説ページアイキャッチ 【Django入門7】リレーションの基本

上記ページで紹介しているウェブアプリでは単なるフォームを利用していますが、今回はこれをモデルフォームを利用する形に変更していきます。具体的には、現状 RegisterFormPostForm を用意しており、これらはそれぞれ UserComment のインスタンスを生成するための情報の入力受付を行うフォームとなっています。これらを単なるフォームクラスではなく、モデルフォームクラスを利用するように変更していきます。

フォームをモデルフォームに置き換えることを示す図

これにより、フォームで別途フィールドの定義を行わなくても、フォームはモデルに合わせたフィールドを自動的に持つようにすることができます。今回は、単純にモデルフォームクラスを利用するための変更を行うため、機能追加を行うものではなく、効率的な開発につなげるための変更となります。

変更方針

前述の通り、現状 RegisterFormPostForm を用意しており、これらはそれぞれ UserComment のインスタンスを生成するための情報の入力受付を行うフォームとなっています。

そのため、RegisterForm に関しては User をベースとして生成するモデルフォームクラスに、PostForm に関しては Comment をベースとして生成するモデルフォームクラスに変更していきます。

用意するモデルフォームとモデルの関係図

モデルクラスの変更

早速モデルフォームクラス側の実装を行なっていきたいところではあるのですが、今までのアプリと同等の妥当性の検証が行われるように、まずはモデルクラスの変更を行います。妥当性の検証はモデルクラスに基づいて実行される で解説したように、今までフォームクラスで行っていた妥当性の検証の設定はモデルクラス側に移行してやる必要があります。

フォームクラスでの妥当性の検証の設定に関しては、下記ページの 掲示板アプリでフォームを利用してみる で行なっています。

Djangoのフォームの解説ページアイキャッチ 【Django入門5】フォームの基本

まず、RegisterFormusername フィールドに対してバリデーターを設定し、そのバリデーターで username が入力データが半角英数字のみから構成されている&先頭が半角英字である場合のみ妥当であると判断するようにしていますので、これに関しては、RegisterForm のベースとなる Userusername フィールドで同様のバリデーターを設定するようにしてやれば良いです。

また、RegisterFormage フィールドに関しては、forms.IntegerFieldmin_value 引数と max_value 引数を指定して最小値と最大値を設定するようにしています。ただし、妥当性の検証はモデルクラスに基づいて実行される で実例を挙げたように、models.IntegerField に関しては min_value 引数と max_value 引数を指定することができないため、代わりにバリデーターを設定して同様の最小値と最大値を設定できるようにしたいと思います。

ですが、これも 妥当性の検証はモデルクラスに基づいて実行される で説明したように、バリデーターを設定しても、その設定はテンプレートへのフォームの埋め込み時に生成される HTML のタグには反映されないため、RegisterForm にコンストラクタを定義し、そこで最小値と最大値の設定を行うようにしたいと思います。

長々と説明してしまいましたが、上記の内容を踏まえ、まずは models.py を下記のように変更したいと思います。

models.py
from django.db import models
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.core.validators import MinValueValidator, MaxValueValidator

min_age = 0
max_age = 200

def check_username(username):
    if not username.isalnum() or not username.isascii():
        raise ValidationError(_('usernameにはアルファベットと数字のみ入力可能です'))
    if not username[0].isalpha():
        raise ValidationError(_('usernameの最初の文字はアルファベットにしてください'))

class User(models.Model):
    username = models.CharField(max_length=32, validators=[check_username])
    email = models.EmailField()
    age = models.IntegerField(validators=[MinValueValidator(min_age), MaxValueValidator(max_age)])
    
    def __str__(self):
        return self.username

class Comment(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='comments')
    text = models.CharField(max_length=256)
    date = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.text[:10]

check_username 関数は、元々 forms.py に定義していたものを移動させていただけになります。

また、age フィールドを見ていただければ分かる通り、最小値や最大値を設定するためには validators 引数に MinValueValidator(最小値)MaxValueValidator(最大値) を指定してやれば良いです。

スポンサーリンク

モデルフォームクラスの定義

続いて、このページの本題となるモデルフォームクラスを定義していきたいと思います。下記ページで示したアプリでは、ユーザー登録フォーム RegisterForm とコメント投稿フォーム PostForm を定義しています。

Django のリレーションについての解説ページアイキャッチ 【Django入門7】リレーションの基本

前述の通り、RegisterFormUser をベースとするモデルフォームクラスに、PostFormComment をベースとするモデルフォームクラスに置き換えていきます。

モデルフォームクラスの定義 で解説したように、モデルフォームクラスは ModelForm を継承して作成し、ベースとするモデルクラスを model に指定する必要があります。あとは、フォームに持たせるフィールドの選択も行う必要がありますので、RegisterForm には User の持つフィールド全てを、PostForm には Comment の持つ date 以外のフィールド、つまり usertext フィールドを持たせるようにしたいと思います。date を除外するのは、date はレコード新規保存時に自動的に日時が設定されるフィールドになっているからです。

ちなみに、user フィールドは、その Comment のインスタンスからリレーションを構築する相手となる User のインスタンスを選択するためのフィールドとなります。また、せっかくなので、text フィールドは、文章入力用のウィジェットに変更してみようと思います。

さらに、下記ページの 掲示板アプリでフォームを利用してみる で作成したフォームと同様に、age フィールドに関しては入力整数の妥当性を検証できるよう、入力フィールドのタグ属性に minmax を設定するようにしたいと思います。

Djangoのフォームの解説ページアイキャッチ 【Django入門5】フォームの基本

上記のようなモデルフォームクラスは、次のように forms.py を変更することで実現することができます。

forms.py
from django import forms
from .models import User, Comment, max_age, min_age

class RegisterForm(forms.ModelForm):

    class Meta:
        model = User
        fields = '__all__'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['age'].widget.attrs.update({
            'min': min_age,
            'max': max_age,
        })

class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        exclude = ['date']

        widgets = {
            'text': forms.Textarea
        }

ビューの変更

最後にビュー側の変更も行なっておきましょう!

現在、ビューでは register_view 関数と post_view 関数とでフォームを利用するようになっています。上記の変更によって、これらの関数から利用されるフォームがモデルフォームクラスのものに変化したことになります。

その変化に伴い、ビュー側も変更したいと思います。ただし、基本的な扱い方はフォームクラスと同じ で解説したように、フォームクラスもモデルフォームクラスも基本的な扱い方は同じなので、現状のビューでも正常に動作はしてくれるはずです。

ですが、save メソッドでデータベースへの保存が可能 で解説したように、モデルフォームクラスを利用するようになったことで、フォームから直接データベースへの保存が行えるようになっています。つまり、今までフォームからデータを取得し、それをモデルクラスのインスタンスにセットしてからデータベースへの保存を行なっていたのですが、このような段階的な処理が不要となり、フォームから保存を行えば良いだけになります。これを利用することで、ビューの実装を楽に行うことができるようになります。

この楽さを実感していただくため、ビューの変更を行なっていきたいと思います。

前述の通り、現状の views.py においては register_viewpost_view からフォームを利用していますので、この2つの関数のみの変更を行なっていきます。具体的には、views.pyregister_viewpost_view を下記のように変更します。

views.pyの一部
def register_view(request):

    if request.method == 'POST':

        form = RegisterForm(request.POST)
        if form.is_valid():
            form.save()

            return redirect('users')

    else:
        form = RegisterForm()
    
    context = {
        'form': form
    }

    return render(request, 'forum/register.html', context)

def post_view(request):
    if request.method == 'POST':
        form = PostForm(request.POST)

        if form.is_valid():
            form.save()

            return redirect('comments')
    else:
        form = PostForm()

    context = {
        'form': form,
    }

    return render(request, 'forum/post.html', context)

変更点は register_viewpost_view とでほぼ同じなので、register_view の方に注目して解説していきます。まず、元々の register_view においては、is_valid メソッドの返却値が True の場合に実行するレコードの保存は下記のような処理で実現していました。。

register_viewでの保存処理(変更前)
if form.is_valid():

    username = form.cleaned_data.get('username')
    email = form.cleaned_data.get('email')
    age = form.cleaned_data.get('age')

    user = User(username=username, email=email, age=age)
    user.save()

    return redirect('users')

上記においては、まず、form から各種フィールドのデータを取得し、さらに取得したデータからモデルクラスのインスタンスを生成しています。そして、その後にインスタンスの保存を save メソッドで行うようになっています。ポイントは、通常のフォームクラスの場合、フォームとモデルは独立しているため、上記のように一旦フォームからデータを取得し、それをモデル側に設定する必要があるという点になります。

それに対し、モデルフォームクラスの場合は、モデルからフォームが生成されていて互いに関連性を持っているため、モデルフォームクラスのインスタンスに save メソッドさえ実行させれば各種フィールドのデータがセットされた状態のインスタンス(レコード)がデータベースに保存されることになります。

なので、実装が下記のように簡素化され、実装が楽になります。特にフォームで扱うフィールドが多くなればなるほど、モデルフォームを利用することでビューの実装は楽になります。

register_viewでの保存処理(変更後)
if form.is_valid():
    form.save()

    return redirect('users')

また、通常のフォームクラスの場合、モデルクラスを変更すれば、それに伴ってフォームクラスの変更も必要となり、さらにそれに伴ってビューの変更も必要となります。例えば、フォームクラスにフィールドを追加すれば、その分ビューで行うフィールドからのデータの取得処理も追加が必要となります。

ですが、モデルフォームクラスの場合は、上記のようにモデルフォームクラスのインスタンスから save メソッドを実行するようにしておけば、モデルクラスを変更してもビューの変更が不要となります。また、モデルフォームクラス自体に関しても、fields 属性に '__all__' を指定している場合や exclude 属性を指定している場合は、モデルクラスにフィールドを追加すれば対応するフィールドがモデルフォームクラス側にも自動的に追加されることになります。

このように、モデルフォームクラスを利用することでビューの実装も楽になりますし、モデルクラスの変更に伴うフォームの変更に関しても最小限に抑えることが可能です。そして、これらはバグを減らしたりコードの再利用性を高めるメリットにつながりますので、特にモデルのフィールドと対応するフォームを作成するような場合は、積極的にモデルフォームの仕組みを利用することをオススメします。

動作確認

最後に、今回実装した内容に関して動作確認を行なっておきましょう!

マイグレーションの実行

今回は models.py の変更を行なっていますので、最初にマイグレーションを実行しておきたいと思います。

マイグレーションは、プロジェクトフォルダの直下、つまり、manage.py が存在するフォルダで下記コマンドを実行することで行うことができます。

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

今回は models.py を変更して各種モデルクラスに妥当性の検証用の設定を追加しただけなので、エラーもなく正常に上記2つのコマンドが完了するのではないかと思います。

開発用ウェブサーバーの起動

マイグレーションが完了した後は、いつも通り Django 開発用ウェブサーバーの起動を行います。マイグレーションを実行した時と同じフォルダで下記コマンドを実行すれば、Django 開発用ウェブサーバーが起動します。

% python manage.py runserver

ユーザー登録フォームの確認

今回は、今まで通常のフォームクラスを利用していたものをモデルフォームクラスを利用するように変更しただけですので、ここではフォームの確認のみを行なっていきたいと思います。

まずはウェブブラウザから下記の URL にアクセスしてみてください。

http://localhost:8000/forum/register/

これにより、ユーザー登録フォームが表示されると思います。といっても、前回の動作確認時とフィールドや見た目は変わらないはずです。

動作確認時に表示されるユーザー登録フォーム

まず、実際にユーザー登録を行い、登録したユーザーがユーザー一覧に表示されることを確認してみてください。

また、再度ユーザー登録フォームを表示し、次は username に数字から始まるユーザー名を入力して送信ボタンを押してみてください。また、同様に、age0 から 200 以外の整数を入力して送信ボタンを押してみてください。

前者の場合は、送信ボタンを押した後に再度フォームが表示され、そこに注意文が示されていること、さらに後者の場合はデータの送信が出来なくなっていることが確認できると思います。

妥当性の検証が動作している様子

これらはモデルクラス自体やモデルフォームクラスに設定された妥当性の検証の結果であり、これらの動作結果より、妥当性の検証も意図した通りに動作していることが確認できたことになります。

コメント投稿フォームの確認

次はウェブブラウザから下記の URL にアクセスしてみてください。

http://localhost:8000/forum/post/

これにより、コメント登録フォームが表示されると思います。

動作確認時に表示されるコメント投稿フォーム

今回、forms.py で text フィールドのウィジェットとして Textarea を指定しているため、text フィールドの見た目が変化していることが確認できると思います。ですが、機能的には前回動作確認時と全く同じになっているはずです。user で投稿者となるユーザーを選択し、適当に text フィールドにテキストを入力してコメント投稿を行なってみてください。

こちらも、投稿したコメントが投稿一覧に表示されれば動作確認 OK となります。

スポンサーリンク

まとめ

このページでは Django におけるモデルフォームについて解説しました!

モデルフォームはフォームをモデルに基づいて自動的に定義する仕組みであり、モデルに基づいたフォームを作成できるようになることでフォームを楽に定義することができるようになりますし、モデル側の変更も自動的にフォーム側に反映されるようにもなります。

別に通常のフォームさえ利用できればモデルフォームを利用しなくてもアプリを開発することは可能ではありますが、効率的に開発を進めるという意味では開発者にとってモデルフォームは強力な仕組みとなると思います。特にモデルとフォームのフィールドは直結することも多いため、そういう場合はモデルフォームを積極的に利用してみましょう!

次の連載ではウェブアプリへのログイン機能の搭載手順について解説します。ウェブアプリではログイン機能を搭載しているものが多いですよね?!ログイン機能の搭載手順を知っていれば、そういったウェブアプリもあなた自身の手で開発することができるようになります。ぜひ次の連載のページも読んでみてください!

Djangoのウェブアプリに対するログイン機能搭載手順の説明ページアイキャッチ 【Django入門9】アプリにログイン機能を搭載する

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

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です