matobaの学んだこと

とあるPythonエンジニアのブログ。ソフトウェア開発、執筆活動、ライフログ。

djangoのformって結局なんなの?という疑問が解消されたのでformを解説します

あー、djangoのformってそういう役割なのかー、というのが自分の中で少し腹落ちしたのでブログに書いておきたいと思います。

ちなみに、Python3.6.5とDjango2.1.4で動作確認してます。

djangoのformはバリデータ

今まで、djangoのformがあるメリットがあんまりよくわかってませんでした。 なんというか、雰囲気でformに関わっていました。

それが、djangoのformは、validatorを効率的に書く仕組みなんだなというのがわかりました。

ここでいうvalidatorというのは、あるデータセットが条件を満たしているかを検証する何かです。

djangoのformはdictを渡して検証できる

djangoのformは、dictに値を詰めて渡すと、特定のキーにどういう値が入っているか、を検証することができます。

dictっていろんなところから渡って来るんですが、それぞれの値にどういう情報が入っていて欲しいか、を検証したいタイミングってありますよね。

特に、アプリの責任分界を明らかにしたい時とか。

簡単な状況なら必要ない

例えば、以下のような状況があったとします。

  • あるdictに、key1が入ってるいることを確認したい

これだけだと、こういう風に in を使って確認できます。

>>> sample_dict = {}
>>> 'key1' in sample_dict
False
>>> sample_dict['key1'] = 'test value'
>>> 'key1' in sample_dict
True

まあ、簡単な状況ならこんな感じで終わりですよね。

ちょっと複雑になると便利

でも、以下のような状況だったらどうでしょう?

  • あるdictにkey1, key2, key3が入ってるいることを確認したい
  • あるdictのkey1, key2, key3はそれぞれ10桁以下の文字列であることを確認したい
  • 問題のあるkeyがあった場合は、どのキーで問題があったのか、何が問題なのかを知りたい

ifで頑張るとちょっとめんどくさそうだなあ、となりませんか? 僕はなります。

そういう時にdjangoのformが便利です。

まず、こういうフォームを書きます。 sample_form/forms.py におきました。

from django import forms


class SampleForm(forms.Form):
    key1 = forms.CharField(max_length=10)
    key2 = forms.CharField(max_length=10)
    key3 = forms.CharField(max_length=10)

動かして見ましょう。

$ python manage.py shell
>>> from sample_form.forms import SampleForm
>>> test_data = {}
>>> obj = SampleForm(test_data) # 空のデータを検証対象のデータとして渡す。
>>> obj.is_valid()  # 検証して結果を見る。当然False
False 
>>> obj.errors # 検証のエラー結果を見ると、各値が必須であることが返って来る。
{'key1': ['This field is required.'], 'key2': ['This field is required.'], 'key3': ['This field is required.']}
>>> test_data['key1'] = 'valid val' #key1に妥当な値を入れる
>>> test_data['key2'] = 'invalid value, because long message' #key2に長すぎる値を入れる
>>> obj = SampleForm(test_data)  #再度検証対象のデータとして渡して作り直す。
>>> obj.is_valid() # 検証して結果を見る。当然False
False
>>> obj.errors # key1でエラーが消える。key2はメッセージが変わる。
{'key2': ['Ensure this value has at most 10 characters (it has 35).'], 'key3': ['This field is required.']}
>>> test_data['key2'] = 'valid val2'
>>> test_data['key3'] = 'valid val3'
>>> obj = SampleForm(test_data)
>>> obj.is_valid()
True
>>> obj.errors
{}

うん。便利ですね。

ちなみにデータにアクセスするときはこうです。

>>> obj.cleaned_data
{'key1': 'valid val', 'key2': 'valid val2', 'key3': 'valid val3'}

こんな感じで、複数のデータと条件だったとしても、サクッと検証することができます。

文字列以外も検証したい

あと、他に各フィールドに入っている値が文字列じゃない場合もありますよね。

例えば、以下のような感じです。

  • email というフィールドには、メールアドレスとして正しい形式のデータが入っていて欲しい
  • created_date というフィールドには、正しい形式の日付が入っていて欲しい
  • url というフィールドにはURLとして正しい形式のデータが入っていて欲しい

こういうのを自分で検証しようとすると結構めんどくさいですよね。

それは以下のようなことを考えないといけないからです。

  • メールアドレスとして正しい形式とは・・・?
  • 日付として正しい形式とは・・・?
  • URLとして正しい形式とは・・・?

きちんとやろうとすると詳細な仕様を調べることになるんですが、正直調べるのめんどくさいですよね。それがformを使うと簡単にできます。

sample_form/forms.py に以下を追加します。

class SampleForm2(forms.Form):
    email = forms.EmailField()
    created_date = forms.DateField()
    url = forms.URLField()

動かして見ます。

>>> from sample_form.forms import SampleForm2
>>> test_data = {}
>>> test_data['email'] = "this is not mail address." # ダメなメールアドレス
>>> test_data['created_date'] = "invalid date string"  # ダメな日付
>>> test_data['url'] = "httpexamplecom"  # ダメなURL
>>> obj = SampleForm2(test_data)
>>> obj.is_valid()
False
>>> obj.errors
{'email': ['Enter a valid email address.'], 'created_date': ['Enter a valid date.'], 'url': ['Enter a valid URL.']}
>>> test_data['email'] = "sample@example.com" # 正しいメールの形式
>>> test_data['created_date'] = "2018-12-25"  # 正しい日付の形式
>>> test_data['url'] = "http://example.com"  # 正しいURLの形式
>>> obj = SampleForm2(test_data)
>>> obj.is_valid()
True
>>> obj.errors
{}

便利ですね。

複数のデータの関係で検証したい

もっと複雑な話になって来ると、以下のような要件も出て来ると思います。

  • title は必須入力にしたい。そのほかは任意項目。
  • check がTrueなら、 comment は必須入力にしたい。
  • comment が入力されるなら、 name は必須入力にしたい。

以下のような形で追加します。

from django import forms
from django.core.exceptions import ValidationError

class SampleForm3(forms.Form):
    title = forms.CharField(max_length=10)
    check = forms.BooleanField(required=False)
    comment = forms.CharField(required=False, max_length=256)
    name = forms.CharField(required=False, max_length=20)

    def clean(self):
        if self.cleaned_data['check'] and not self.cleaned_data['comment']:
            raise ValidationError('チェック入れたらcommentは必須です', code='invalid')

        if self.cleaned_data['comment'] and not self.cleaned_data['name']:
            raise ValidationError('comment入れたらnameは必須です', code='invalid')

使って見ます。

>>> from sample_form.forms import SampleForm3
>>> test_data = {}
>>> test_data['title'] = 'sample'
>>> obj = SampleForm3(test_data)
>>> obj.is_valid() #titleしか入ってないときは検証OK
True
>>> test_data['check'] = True
>>> obj = SampleForm3(test_data)
>>> obj.is_valid() #chekを入れると検証NGになる
False
>>> obj.errors #ダメな理由はコメントがないから
{'__all__': ['チェック入れたらcommentは必須です']}
>>> test_data['comment'] = "aaaaaaaa"
>>> obj = SampleForm3(test_data)
>>> obj.is_valid() #コメントを入れて見るとnameがないエラーが出る。
False
>>> obj.errors
{'__all__': ['comment入れたらnameは必須です']}
>>> test_data['name'] = "hoge"
>>> obj = SampleForm3(test_data)
>>> obj.is_valid()
True
>>> obj.errors
{}

ふむふむ。

でも結局、DBに入れるんですよね?

はい。まあ大抵の場合は検証したあとそのままDBに入れたりするわけです。

となると、formの値がmodelにセットされたらいいなーという想いを組んでくれるのが、ModelFormです。

こういうモデルがあったとします。 sample_form/models.py

from django.db import models


class Blog(models.Model):
    title = models.CharField(max_length=20)
    content = models.TextField(max_length=20)

そういう時にこうformを書く。 sample_form/forms.py

from django import forms
from sample_form.models import Blog

class BlogForm(forms.ModelForm):
    class Meta:
        model = Blog
        fields = "__all__"

するとこうなる。

>>> from sample_form.forms import BlogForm
>>> test_data = {}
>>> test_data["title"] = 'test'
>>> obj = BlogForm(test_data)
>>> obj.is_valid() # 普通にバリデーションできる
False
>>> obj.errors
{'content': ['This field is required.']}
>>> test_data["content"] = 'test content'
>>> obj = BlogForm(test_data)
>>> obj.is_valid()
True
>>> obj.save() # そのまま保存できる。
<Blog: Blog object (1)>
>>> from sample_form.models import Blog
>>> Blog.objects.all()
<QuerySet [<Blog: Blog object (1)>]>
>>> Blog.objects.first().title #当然保存されてる。
'test'

うん。便利ですね。

終わり

今回は、今まで雰囲気で使ってきたdjangoのformをもう少し理解しようと思って解説して見ました。

誰かのdjangoのformの理解の役に立てば幸いです。

ではでは。