【Django】フォーム(forms.Form/ModelForm)でバリデーションを実装する

Django

こんにちは、かつコーチです。

前回はQuerySetの基本とフィルタの書き方を解説しました。

今回は、ユーザーからの入力を受け取って検証する「フォーム」の実装方法を扱います。

Djangoにはforms.Formforms.ModelFormという2種類のフォームがあり、使い分けを理解しておくと実装がぐっと楽になります。

forms.FormとModelFormの違い

2種類のフォームの使い分け

Djangoのフォームには、大きく分けて2つの種類があります。

クラス用途
forms.Formモデルに直結しない入力(お問い合わせフォーム、検索フォームなど)
forms.ModelFormモデルの保存・更新を前提とした入力(記事投稿フォームなど)

「保存先のモデルが決まっているか」で使い分けるとシンプルです。

モデルに保存するデータを扱うフォームなら、ほぼModelForm一択と考えて問題ありません。

なぜフォームを使うのか

request.POSTから直接値を取り出して処理することも技術的には可能ですが、バリデーション(入力値が正しいかどうかのチェック)を自前で書くのは手間がかかり、抜け漏れも起きやすくなります。

Djangoのフォームを使えば、型変換・必須チェック・エラーメッセージの表示までまとめて任せられます。

forms.Formの基本の書き方

フォームクラスの定義

forms.Formは、forms.pyにクラスとして定義します。

# forms.py
from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(label="お名前", max_length=100)
    email = forms.EmailField(label="メールアドレス")
    message = forms.CharField(label="お問い合わせ内容", widget=forms.Textarea)

CharFieldEmailFieldといったフィールドは、モデルのフィールドと似た感覚で書けます。

ビューでの利用方法

# views.py
from django.shortcuts import render, redirect
from .forms import ContactForm

def contact(request):
    if request.method == "POST":
        form = ContactForm(request.POST)
        if form.is_valid():
            # form.cleaned_dataにバリデーション済みの値が入る
            name = form.cleaned_data["name"]
            email = form.cleaned_data["email"]
            # ここでメール送信などの処理を行う
            return redirect("contact_done")
    else:
        form = ContactForm()

    return render(request, "contact.html", {"form": form})

form.is_valid()を呼ぶと、バリデーションが実行されてTrue/Falseが返ります。

検証済みの値はform.cleaned_dataから取り出す点がポイントです。

ModelFormでモデルと連動させる

基本の書き方

モデルの保存を目的とするフォームは、ModelFormを使うとコード量を大幅に減らせます。

# models.py
from django.db import models

class Article(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    is_published = models.BooleanField(default=False)


# forms.py
from django import forms
from .models import Article

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body", "is_published"]

Metaクラスで対象モデルと、フォームに含めるフィールドを指定するだけで、モデル定義から自動的にフォームフィールドが生成されます。

保存処理はform.save()にまとめられる

def article_create(request):
    if request.method == "POST":
        form = ArticleForm(request.POST)
        if form.is_valid():
            form.save()  # モデルインスタンスの作成・保存までまとめて行う
            return redirect("article_list")
    else:
        form = ArticleForm()

    return render(request, "article_form.html", {"form": form})

forms.Formではcleaned_dataから自分でArticle.objects.create()する必要がありますが、ModelFormならform.save()一発でモデルの保存まで完結します。

編集フォームにしたい場合は、ArticleForm(request.POST, instance=article)のように既存インスタンスを渡すだけで対応できます。

バリデーションをカスタマイズする

clean_フィールド名()で個別チェックを追加する

特定のフィールドに独自のチェックを加えたい場合は、clean_<フィールド名>メソッドを定義します。

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        fields = ["title", "body", "is_published"]

    def clean_title(self):
        title = self.cleaned_data["title"]
        if "テスト" in title:
            raise forms.ValidationError("タイトルに「テスト」という文字列は使用できません")
        return title

このメソッドはis_valid()が呼ばれたタイミングで自動的に実行され、ValidationErrorを投げるとエラーメッセージがフォームに表示されます。

clean()で複数フィールドをまたぐチェックを行う

「開始日が終了日より後になっていないか」のように、複数フィールドにまたがるチェックにはclean()メソッドを使います。

def clean(self):
    cleaned_data = super().clean()
    start_date = cleaned_data.get("start_date")
    end_date = cleaned_data.get("end_date")

    if start_date and end_date and start_date > end_date:
        raise forms.ValidationError("開始日は終了日より前の日付にしてください")

    return cleaned_data

必ずsuper().clean()を呼んでから処理する点を忘れないようにしましょう。

つまずきやすいポイント:テンプレートでエラーが表示されない

form.is_valid()がFalseなのに何も表示されない

筆者は最初、バリデーションエラーが起きているのにテンプレートに何も表示されず、原因がわからず悩んだことがあります。

❌ Before:テンプレートでフォームフィールドを個別にべた書きしている

<form method="post">
  {% csrf_token %}
  <input type="text" name="title">
  <button type="submit">送信</button>
</form>

これでは、Djangoが管理しているエラーメッセージがどこにも紐づかず、is_valid()Falseでも画面上には何も表示されません。

✅ After:{{ form }}やas_pで自動生成し、エラーも一緒に表示する

<form method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">送信</button>
</form>

{{ form.as_p }}と書けば、各フィールドに対応するエラーメッセージ(このフィールドは必須です。など)も自動で表示されるようになります。

個別にレイアウトを組みたい場合も、{{ form.title }}の直後に{{ form.title.errors }}を置くことで、フィールド単位のエラー表示ができます。

まとめ

この記事のポイント

  • モデルに保存しない入力はforms.Form、モデル保存前提ならforms.ModelForm
  • ModelFormMetaクラスでモデルとフィールドを指定するだけで自動生成できる
  • 個別フィールドの検証はclean_<フィールド名>、複数フィールドの検証はclean()
  • テンプレートでは{{ form.as_p }}などを使い、エラーメッセージを確実に表示する

次に読むべき記事

フォームでデータを扱えるようになったら、次は一覧表示で必ず意識したいN+1問題とその解決方法を解説します。

→ 次の記事:N+1問題とselect_related・prefetch_relatedでの解決方法

タグ: #Django #中級者向け #データベース

タイトルとURLをコピーしました