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

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

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

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

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

以上が、最低限モデルフォームクラスを定義する上で必要となる手順となります。このような手順でクラスを定義すれば、そのクラスはモデルフォームクラスとして扱われ、このクラスは、model 属性に指定したモデルクラスの持つフィールドのうち、fileds 属性で指定したフィールドを持つフォームとして扱われます(もしくは exclude 属性で指定しなかったフィールドを持つ)。

そして、このモデルフォームクラスは、基本的には通常のフォームクラスと同様にしてビューやテンプレートから扱うことができます。例えば、テンプレートファイルにモデルフォームクラスのインスタンスを埋め込めば、そのインスタンスの持つフィールドを表示するためのタグが HTML に埋め込まれることになります。ただ、これらにも多少扱い方が異なる点はあるので、この点に関しては モデルフォームクラスでフォームを扱う で解説していきたいと思います。

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

前述のとおり、ここまで説明した 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やwidgetsの指定例
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 クラスと同様のフィールドを持つフォームクラスは下記のようにして定義することができます。

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

class UserModelForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['name', 'email', 'age', 'height', 'weight']

fields の指定等は必要になりますが、クラス変数等の定義が減って実装量が減り、コードの見た目もシンプルになったことが確認できると思います。

スポンサーリンク

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

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

次は、モデルフォームクラスの扱いについてのポイントを解説していきます。

前提として、フォームクラスもモデルフォームクラスも両方とも BaseForm というクラスのサブクラスとなります。同じクラスを継承しており、基本的な扱い方はフォームクラスの場合もモデルフォームクラスの場合も同様になります。ただ、フォームクラスには存在しない、モデルフォームクラス特有の使い方や機能が存在しますので、このあたりを中心に解説していきます。

MEMO

ここからは、ビューの関数がリクエストを受け取る引数が request であることを前提に解説を進めていきます

インスタンスの生成

まず、モデルフォームクラスのインスタンスの生成の仕方について解説していきます。

モデルフォームクラスにおいても、フォームクラス同様に、コンストラクタを引数なし or  引数にrequest.POST を指定して実行することでインスタンスを生成することが可能です。モデルフォームクラスのインスタンスの各種データ属性を初期値(空)の状態でインスタンスを生成したい場合は前者で、モデルフォームクラスのインスタンスの各種データ属性にフォームから送信されてきた各種フィールドのデータをセットした状態でインスタンスを生成したい場合は後者を利用することになります。

モデルからのモデルフォームクラスのインスタンスの生成

上記に加え、モデルフォームクラスのインスタンスに関しては、モデルクラスのインスタンスから生成することも可能です。

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

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

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

更新フォームでは、ユーザーに更新前のレコードの中身を確認してもらうために、更新前の各種フィールドの値をセットした状態のフォームを表示することが多いです。そして、このようなフォームは、instance 引数へ “データベースから取得したレコード(モデルクラスのインスタンス)” を指定してモデルフォームクラスのコンストラクタを実行することで生成できます。あとは、このインスタンスを、通常のフォームクラスのとき同様にテンプレートファイルに埋め込めば、更新前の各種フィールドの値がセットされた状態のフォームを表示可能な HTML が生成されることになります。

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

例えば、UserModelFormUser というモデルクラスをベースとするモデルフォームクラスとした場合、User に対応するレコードの更新フォームの表示は、下記のような update 関数によって実現することができます。app/update.html はフォームを表示するためのテンプレートファイルであることを想定しています。さらに、更新対象のレコードの id 引数 id で渡されるものとして関数を定義しています。

更新フォームの表示
from django.shortcuts import render
from .forms import UserModelForm
from .models import User

def update(request, id):
    # idが引数idと一致するUserのインスタンスを取得
    user = User.objects.get(id=id)

    # userからUserModelFormのインスタンスを生成
    form = UserModelForm(instance=user)

    # userの更新フォームを表示
    context = {
        'form' : form
    }
    return render(request, 'app/update.html', context)

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

request.POSTinstance 両方からのインスタンスの生成

また、モデルフォームクラスのコンストラクタは、request.POST と instance 引数の両方を指定して実行することも可能です。これにより、instance 引数で指定したレコード(モデルクラスのインスタンス)の各種フィールドが、フォームから送信されてきたデータ(request.POST)の各種フィールドの値で上書きされた状態のモデルフォームクラスのインスタンスが生成されることになります。

request.POSTとinstance引数を指定してモデルフォームクラスのインスタンスを生成する様子

そして、このモデルフォームクラスのインスタンスに、次の save メソッドによるデータベースへの保存 の節で紹介する save メソッドを実行させれば、データベース内の instance 引数で指定したレコードが更新されることになります。

なので、このようなインスタンスの生成は、先ほどと同様にレコードの更新フォームを実現する時、具体的には、レコードの更新フォームからデータが送信されてきたときに利用されることが多いです。

request.POSTとinstance引数の指定によって生成したフォームクラスのインスタンスでのsaveメソッド実行がレコードの更新の意味になる理由を説明する図

例えば、先程示した update 関数を下記のように変更すれば、メソッドが GET 以外の場合に、引数で指定された id と一致する id を持つレコードが、フォームから送信されてきたデータで更新されることになります。 ちなみに、メソッドが GET の場合は、変更前の update 関数と同じ処理、すなわち、引数で指定された id と一致する id を持つレコードの更新フォームの表示が行われることになります。

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

def update(request, id):
    # idが引数idと一致するUserのインスタンスを取得
    user = User.objects.get(id=id)
    if request.method == 'GET':
        # userからUserModelFormのインスタンスを生成
        form = UserModelForm(instance=user)

    else:
        # userをrequest.POSTで更新したUserModelFormのインスタンスを生成
        form = UserModelForm(request.POST, instance=user)

        # データベースのuserを更新
        if form.is_valid():
            form.save()
            return 略

    # userの更新フォームを表示
    context = {
        'form' : form
    }
    return render(request, 'app/update.html', context)

ここでポイントになるのが、コンストラクタへの引数の指定の仕方によって save メソッドの動作が異なるという点になります。

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

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

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

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

このように、モデルフォームクラスではフォームクラスに比べてコンストラクタの実行の仕方のバリエーションが多く、使い方によっては上記のようなレコードの更新等を簡単に実現することができます。まずは、instance 引数が指定可能であることは覚えておきましょう!

save メソッドによるデータベースへの保存

次に、モデルフォームクラスの save メソッドについて解説します。

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

通常のフォームクラスには save メソッドは用意されていないため、フォームから送信されてきたデータのデータベースへの保存はモデルクラスの save メソッドで行う必要があります。そのため、下記のような手順でデータベースへの保存を行う必要がありました(下記はレコードの新規保存時の手順となります)。

  1. request.POST をコンストラクの引数に指定してフォームクラスのインスタンスを生成
  2. フォームクラスのインスタンスで is_valid メソッドを実行
  3. フォームクラスのインスタンスのデータ属性 cleaned_data から各種フィールドのデータを取得
  4. モデルクラスのインスタンスの各種データ属性に、3. で取得したデータをセット
  5. モデルクラスのインスタンスで save メソッドを実行

面倒なのは 3. と 4. で、これらの処理は各種フィールドに対して1つ1つ行う必要があります。そして、それを行ってからモデルクラスの save メソッドで保存を実行することになります。

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

それに対し、モデルフォームクラスには save メソッドが用意されており、フォームから送信されてきたデータのデータベースへの保存は、下記のように直接モデルフォームクラスの save メソッドを実行することで実現できます。

  1. request.POST をコンストラクの引数に指定してモデルフォームクラスのインスタンスを生成
  2. モデルフォームクラスのインスタンスで is_valid メソッドを実行
  3. モデルフォームクラスのインスタンスで 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のインスタンスを取得

この方法でのインスタンス取得の注意点は、基本的にはデータベースへの保存を行った後にしかモデルクラスのインスタンスが取得できないという点になります。データベースへの保存を行う前にモデルクラスのインスタンスを取得したいのであれば、次の データ属性 instance から取得する で紹介する方法を採用すればよいです。もしくは、save メソッドの引数に commit=False を指定することでも、データベースへの保存無しにモデルクラスのインスタンスの取得を実現することが可能です。

ただし、commit=False はデータベースへの保存をスキップするための引数指定となりますので、save メソッドでデータベースへの保存が行われなくなります。したがって、データベースへの保存が必要であれば、commit=False を引数に指定せずに、再度 save メソッドを実行する必要があります。

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

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

モデルフォームクラスのインスタンスはデータ属性 instance を持っており、この instancemodel に指定したモデルクラスのインスタンスとなります。

ですので、このデータ属性 instance から直接モデルクラスのインスタンスを取得することができます。ただし、フォームから送信されてきたデータは、安全性を考慮すると妥当性の検証を行って “妥当である” と判断された場合のみ扱うようにする必要があるため、下記のように is_valid メソッドを実行し、その結果が True である場合のみデータ属性 instance を取得するのが良いと思います。

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のインスタンスを取得

is_valid メソッドによる妥当性の検証

ここまで紹介してきたソースコードでも利用例を示していますが、通常のフォームクラス同様に、モデルフォームクラスにも is_valid メソッドが存在し、この is_valid を実行することでモデルフォームクラスのインスタンスの各種フィールドにセットされたデータの妥当性の検証を実施することが可能です。

通常のフォームクラスの時と同様に、フォームから送信されてきたデータは、まずそのデータの妥当性を検証してから使用する必要があります。これは、フォームから送信されてきたデータを安全に扱うためです。そして、この妥当性の検証は、request.POST を引数に指定してコンストラクタを実行してインスタンスを生成し(instance 引数も指定しても OK)、そのインスタンスに is_valid メソッドを実行させることで行うことが出来ます。

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

ただし、モデルフォームクラスの場合、基本的には妥当性の検証はモデルクラスに基づいて実施されることになります。

通常のフォームクラスの場合、フォームクラス自体にフィールドを持たせるので、フォームクラスの持つフィールドに基づいて妥当性の検証を行わせることが可能です。ですが、モデルフォームクラスの場合は、model に指定したモデルクラスのフィールドに従って自動的にフィールドが追加されることになるため、基本的には model に指定したモデルクラスの各種フィールド(クラス変数)に基づいて妥当性の検証が行われることになります。

MEMO

モデルフォームクラスのフィールドは model に指定したモデルクラスに基づいて追加されることになりますが、新たなフィールドを追加する で説明したように、モデルクラスの持たないフィールドをモデルフォームクラス追加で持たせることもできます

このフィールドに関しても、is_valid メソッド実行時に妥当性の検証が行われることになります

ただし、モデルクラスにおいても、フォームクラスと同様に、各種フィールドに指定する Field のサブクラスの種類に従った妥当性の検証が行われることになります。例えば、下記のようにモデルクラスを定義した場合、”model = User を指定したフォームモデルクラス” のインスタンスによる is_valid メソッド実行時には、ageフィールドの値が整数の場合のみ “妥当である” と判断してくれることになりし、email フィールドの値がメールアドレスの形式として適切である場合のみ “妥当である” と判断してくれることになります。

モデルクラスの定義例
class User(models.Model):
    age = models.IntegerField()
    email = models.EmailField()

また、Field のサブクラスのコンストラクタの引数に validators を追加し、ウェブアプリ特有の妥当性の検証の判断基準を追加するようなことも可能になります。このあたりも通常のフォームクラスの時と同様です。

このように、Field のサブクラスの種類によって適切にデータの妥当の検証が実施される点や、validators 引数の指定によって “妥当であることの判断基準” を開発者が指定可能である点は、フォームクラスでもモデルクラスでも同様となります。

なので、基本的には下記ページの 「妥当である」の判断基準 で解説している内容に基づき、ウェブアプリに必要となる妥当性の検証が行われるようにモデルクラスを定義していけばよいことになります。

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

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

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

フォームクラスでは、forms.Field のサブクラスを利用してフィールドを定義し、モデルクラスでは models.Field のサブクラスを利用してフィールドを定義することになります。つまり、両方とも Field のサブクラスを利用してフィールドを定義するのですが、利用するサブクラスは異なるものになります。なので、それらのサブクラスのコンストラクタに指定可能な引数も異なります。

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

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

フォームのフィールドに対する最小値と最大値の設定
from django import forms

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

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

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

また、クライアント側で妥当性の検証が行われなかったとしても、min_valuemax_value の範囲外の値が入力された場合は、ウェブアプリ側で is_valid メソッドが実行され、その時にデータが妥当でないと判断することができます。

それに対し、models.IntegerField() の場合は引数として min_value max_value は指定不可です。そのため、forms.IntegerField の時と同様の引数指定では最小値と最大値に対する妥当性の検証が実現できないことになります。ただ、下記のように validators 引数および、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)])

この User クラスを model に指定したモデルフォームクラスを定義すれば、そのインスタンスで is_valid メソッドを実行したときに、送信されてきたデータが 0200 の間の値でなければ妥当でないと判断することができます。

なので、引数の指定の仕方は違えど、is_valid メソッドの実行時に同じ判断基準で妥当性の検証を実施することは可能です。

なんですが、上記のように models.IntegerField() に引数を指定したとしても、forms.IntegerField() の引数に min_valuemax_value を指定したときのような、HTML のフィールド要素のタグの属性の設定は行われません。なので、下の図のような、クライアント側のウェブブラウザでの入力値の妥当性の検証は行われないことになります。

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

で、おそらく、models.IntegerField() への引数の指定では、HTML のフィールド用のタグの属性の設定を行うことはできないと思います。つまり、forms.IntegerField で定義するフィールドと models.IntegerField で定義するフィールドとでは実現できることが異なります。こんな感じで、forms.Field のサブクラスと models.Field のサブクラスとでは、名前は似ていても、コンストラクタに指定可能な引数や実現できることが異なります。

ですが、前述のとおり、どちらの場合でも is_valid メソッドでは同じ判断基準で妥当性の検証は行えるので、機能的には問題ないと考えて良いと思います。

フィールドの再定義による妥当性の判断基準の追加

ただ、どうしても forms.Field のサブクラス側と全く同じ機能を実現したいのであれば、一応方法はあります。それは、モデルフォームクラスで、フィールドを再定義する方法になります。

新たなフィールドを追加する で説明した通り、モデルフォームクラスでもフィールドを定義することは可能です。そして、モデルフォームクラスでは、通常のフォームクラスと同様に、forms.Field のサブクラスを利用してフィールドを定義することになります。

さらに、モデルフォームクラスで定義したフィールドのフィールド名が、モデルクラスの持つフィールド名と同じである場合、モデルフォームクラス側で定義したフィールドが優先されることになります。つまり、特定のフィールドを、forms.Field のサブクラスを利用して定義するフィールドに上書きすることができます。で、これをフィールドの再定義と呼んでいます。

なので、どうしても forms.Field のサブクラス側と全く同じ機能を持たせたいフィールドがあるのであれば、そのフィールドをモデルフォームクラスで再定義してやれば良いことになります。

例えば、先ほど話題に挙げた age フィールドを再定義する例は下記のようになります。このように、フィールドを再定義することで、特定のフィールドを forms.Field のサブクラスを利用して定義することができ、通常のフォームクラスで定義するフィールドと全く同じフィールドをモデルフォームクラスに持たせることが出来ます。

ageフィールドの再定義
class UserForm(forms.ModelForm):
    age = forms.IntegerField(min_value=0, max_value=200)
    class Meta:
        model = User
        fields = ['age']

なので、先ほど例に挙げた、下の図のようなクライアント側での妥当性の検証も、モデルフォームクラスで実現できることになります。

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

ということで、このモデルフォームクラス側でのフィールドの再定義を利用すれば、妥当性の検証に関しても、通常のフォームクラス利用時と同じ引数指定で実現できますし、機能的にも全く同じものにすることができます。

ただ、モデルフォームクラス側でフィールドの再定義をたくさんすると、モデルフォームクラスを利用するメリットが減るので、基本的にはモデルクラス側で妥当性の検証の設定を行うようにした方が良いと思います。

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

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

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

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

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

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

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

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

スポンサーリンク

掲示板アプリのプロジェクト一式の公開先

この Django 入門 の連載を通して開発している掲示板アプリのプロジェクトは GitHub の下記レポジトリで公開しています。

https://github.com/da-eu/django-introduction

また、前述のとおり、ここでは前回の連載の 掲示板アプリでリレーションを利用してみる で作成したプロジェクトをベースに変更を加えていきます。このベースとなるプロジェクトは下記のリリースで公開していますので、必要に応じてこちらからプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-relation

さらに、ここから説明していく内容の変更を加えたプロジェクトも下記のリリースで公開しています。ソースコードの変更等を行うのが面倒な場合など、必要に応じて下記からプロジェクト一式を取得してください。

https://github.com/da-eu/django-introduction/releases/tag/django-modelform

モデルクラスの変更

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

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

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

これと同様の妥当性の検証が行われるように、User クラスのクラス変数の右辺側で実行している models.Field のサブクラスのコンストラクタの引数の設定を行います。

ただ、is_valid メソッドによる妥当性の検証 で説明したように、age の右辺の models.IntegerField() に関しては min_value 引数と max_value 引数が指定できません。そのため、is_valid メソッドで今までと同様の妥当性の検証が行われるように、引数 validators=[MinValueValidator(0), MaxValueValidator(200)] の指定を行うようにします。これだと、今までのウェブアプリでは行われていた “クライアント側での最小値・最大値に対する妥当性の検証” が行われないようになってしまうのですが、ここは妥協したいと思います。

MEMO

今回は実施しませんが、is_valid メソッドによる妥当性の検証 で説明しているように、モデルフォームクラスでのフィールドの再定義により、クライアント側での最小値・最大値に対する妥当性の検証も実現可能です

上記の内容を踏まえ、今までフォームクラスで行っていた妥当性の検証の設定をモデルクラスに移行するために、 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

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(0), MaxValueValidator(200)])
    
    def __str__(self):
        return self.username

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

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

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

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

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

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

前述の通り、これらのフォームクラスをモデルフォームクラスに変更していきます。具体的には、RegisterFormUser をベースとするモデルフォームクラスに、PostFormComment をベースとするモデルフォームクラスに置き換えていきます。

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

また、せっかく 各種フィールドをカスタマイズする でウィジェットのカスタマイズについて解説したので、text フィールドは文章入力用のウィジェットに変更するようしたいと思います。

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

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

class RegisterForm(forms.ModelForm):
    class Meta:
        model = User
        fields = ['username', 'email', 'age']

class PostForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['user', 'text']

        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 メソッドを実行するようにしておくことで、モデルクラスを変更してもビューの変更が不要となります。

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

動作確認

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

マイグレーションの実行

今回は 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/

これにより、ユーザー登録フォームが表示されると思います。といっても、前回の動作確認時とフィールドや見た目は変わらないはずです。実際にユーザー登録を行い、今までのウェブアプリと同様に、登録したユーザーがユーザー一覧に表示されることを確認してみてください。

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

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

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

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

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

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

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

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

まとめ

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

モデルフォームはフォームをモデルに基づいて自動的に定義する仕組みであり、モデルに基づいたフォームを作成できるようになることでフォームを楽に定義することができるようになります。また、モデルフォームの導入により、ビューの実装も簡素化することができます。

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

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

ログインの実現方法の解説ページアイキャッチ 【Django入門10】ログイン機能の実現

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

コメントを残す

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