こんにちは、かつコーチです。
前回はQuerySetの基本とフィルタの書き方を解説しました。
今回は、ユーザーからの入力を受け取って検証する「フォーム」の実装方法を扱います。
Djangoにはforms.Formとforms.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)
CharFieldやEmailFieldといったフィールドは、モデルのフィールドと似た感覚で書けます。
ビューでの利用方法
# 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 ModelFormはMetaクラスでモデルとフィールドを指定するだけで自動生成できる- 個別フィールドの検証は
clean_<フィールド名>、複数フィールドの検証はclean() - テンプレートでは
{{ form.as_p }}などを使い、エラーメッセージを確実に表示する
次に読むべき記事
フォームでデータを扱えるようになったら、次は一覧表示で必ず意識したいN+1問題とその解決方法を解説します。
→ 次の記事:N+1問題とselect_related・prefetch_relatedでの解決方法