Django中ModelForm的使用


什么是 ModelForm


ModelForm 是 Django 中编写基于 Model 定制表单的方法,可以提高 Model 复用性。


使用时 Django 会根据 django.db.models.Field (用于数据库衔接) 自动转化为 django.forms.Field (用于表单前端展示、后端验证)。


定义 ModelForm 表单


举一个书籍管理例子,这个例子中定义了



  • title 字段,存 char,用于储存书籍的标题

    • 最大允许长度 20
    • 数据库唯一值,这意味着数据库不能储存两个相同标题的书

  • author 字段,存 Author 的外键,指向储存书籍的作者

Model

class Article(models.Model): 
title = models.CharField(max_length=20, unique=True)
author = models.ForeignKey('Author')

ModelForm

class ArticleForm(forms.ModelForm): 
class Meta:
model = Article

用 Form 来构造的话这个 ModelForm 类似这样

class ArticleForm(forms.Form): 
title = forms.CharField(max_length=20)
author = forms.ModelChoiceField(queryset=Author.objects.all())

定制 ModelForm 表单


ModelForm 提供了自定义 Field、错误信息、渲染方法、正反选择模型的字段等。


如何定制 ModelForm 呢?有两种方式:Meta (ModelForm 独有) 和自定义字段 (普通 Form 中也可使用)。


Meta


ModelForm 通过 Meta 把 db.Field 自动转化为 forms.Field,其中涉及到几步转化



  • validators 不变
  • 添加 widget 属性,即前端的渲染方式
  • 修改 Model 包含的字段,通过 fields 来拿指定字段或者通过 exclude 来排除指定字段
  • 修改错误信息

我们可以通过下面的例子来看一看如何通过 Meta 来自定义 ModelForm 的

class ArticleForm(forms.ModelForm): 
class Meta:
# 指定 Model
model = Article
# Form 需要 Model 中的哪几个 Field
fields = ['title']
# Form 排除 Model 中的哪几个 Field
exclude = ['author']
# 自定义 error_message
error_messages = {
'invalid' = 'invalid title'
}
# 自定义 widget,这里使用了长 80 列,宽 20 行的 textarea
widgets = {
'name': Textarea(attrs={'cols': 80, 'rows': 20}),
}


Meta 的缺点是不能修改字段的 validators,如果需要修改 validators,需要在 Meta 外部重新定义一个同名 Field 来覆盖之



在 Form 中另外定义 Field


这是 Form 中通用的定义 Field 的方法,在 ModelForm 中它有两个作用



  1. 补充 Model 没有的 Field 到表单
  2. 覆盖 Model 中的 Field 定义

且看下面的例子,Article 中已经包含了 title 字段,我们在 ModelForm 中重新定义了它,把 CharField 改为了 ChoiceField,并且自定义了 validators。


注意:覆盖 title 的时候,把 title 从 Meta 中 exclude 掉是可选的,去不去掉的区别在于,你是否需要它为你校验 unique=True 这个数据库级限制。在这里我们需要校验,因为 ModelForm 校验通过后我需要把它存入数据库,如果这里没有校验的话,碰到同标题的书数据库就会在储存时报错,我们希望把这步校验放在 ModelForm 的校验中,而不是在通过校验后再用 try... catch... 来捕获它。


class ArticleForm(forms.ModelForm): 
title = forms.ChoiceFied(choices=((1, '罗宾汉'), (2, '田伯光'),), validators=MaxValueValidator(2))
class Meta:
model = Article

值得一提的一些 Field 转化


AutoField


该 Field 不会出现在 ModelForm 表单中。



所有 editable=False 的 Field 都不会出现在 ModelForm 中。



BooleanField


由于表单提交时统一识别为 string,而 BooleanField 是用 python 中的 bool 来判断的,所以只要传了任意非空值,BooleanField 都会当做 True 来处理,而如果传了空值,由于 forms.Field 默认属性是 required=True,会校验失败,所以如果你需要一个可以填 False 的 Field,那么你需要在 Form
中手动设置这个 Field 的 required=False


ForeignKey


ForeignKey 自动转化为 ModelChoiceField,用下拉选项菜单渲染,默认渲染出来的选项显示为对应 Field 的 str,提交的值为对应 Field 的 id,这些都可以定制。


在后端接收提交的时候会自动在对应的 Model 中用 id 去找,如果没找到则抛出 ValidationError。


ManyToManyField


ManyToManyField 自动转化为 ModelMultipleChoiceField,用多选框渲染,同样默认渲染出来的选项显示为对应 Field 的 str,提交的值为对应 Field 的 id 值。


比如有个叫 group 的 ManyToManyField,选中了 'finance' 'develop' 这两个选项,他们的 id 分别为 1 和 2,那么世界上提交的表单 QueryString 就是 group=1&group=2


初始化 ModelForm 表单

form = ArticleForm(request.POST)



  • 校验的时候可以定义 instance 参数来给 ModelForm 初始化实例,即后续的修改都作用在这个实例上



  • 可以定义 initial 参数来给 ModelForm 初始值,同名 Field 会覆盖 instance 的值

    article = Article.objects.get(pk=1) 
    author = Author.objects.first()
    form = ArticleForm(request.POST, instance=article, initial={‘author’: author})

到这里的时候 form 已经绑定到 article 对象了,并且 author Field 的初始值为 author 对象。

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


这里数据加载的先后顺序为 instance < initial < request.POST




校验 ModelForm 表单



注意:Form 只会检查内部定义过的 Field,request.POST 中其余 keyword 都会被无视和过滤掉,即不会出现在返回的 cleaned_data 中。


form = ArticleForm(request.POST)

校验表单

if form.is_valid():
# 保存到数据库
article = form.save()

is_valid() 会调用 full_clean()来对表单进行全面校验,它又分成三步(定义在基类 Form)中



  1. 根据每个 Field 来做单个 Field 的校验 ( 比如 title 字段就会校验是否超出最大允许长度 20 ) 其中在 Field.clean() 执行过后提供了钩子 clean_[field_name]
  2. 根据 Form 定义的 Field 之间的依赖关系做整个表单的校验,钩子方法为 clean()
  3. 自定义校验通过后的表单处理,钩子叫 _post_clean()

    • 这一步中,ModelForm 做了一些额外的检验:如果定义在 Meta 中的 Field 有 unique=True 这个限制,那么 ModelForm 则会按照现有数据库中的数据对其校验,看这个 Field 的值是否已存在,如果已存在,则抛出一个 IntegrityError。实际操作中如果强制不校验 unique 的话,可以把该字段从 Meta 中移除,在 ModelForm 中重新定义该字段。


储存 ModelForm 对象


调用 save() 的时候可以添加 commit=False 来避免立即储存,从而通过后续的修改或补充来得到完整的 Model 实例后再储存到数据库。


如果初始化的时候传入了 instance,那么调用 save() 的时候会用 ModelForm 中定义过的字段值覆盖传入实例的相应字段,并写入数据库。


save() 同样会帮你储存 ManyToManyField,如果 save 时使用了 commit=False,那么 ManyToManyField 的储存需要等该条目存入数据库之后手动调用 ModelForm 的 save_m2m() 方法。


使用同一表单来 新建 | 更新 指定实例


通常的步骤分为如下几步



  • 检测该对象是否已在数据库

    • 如果已存在,那么需要手动获取该实例,然后更改相关 field 内容,最后使用 update() 方法保存到数据库
    • 如果不存在,新建一个 Model 实例并修改至完整的 Model,调用 save() 方法保存到数据库

f = AuthorForm(request.POST) 
if f.is_valid():
try:
# Save the new instance.
new_author = f.save(commit=False)
new_author.some_field = f.cleaned_data['some_field']
new_author.save()
except IntegrityError:
# 已存在
# 若要这样使用 update 的话需要在 cleaned_data 中加入上述 some_field 的改动
# 因为 some_field 的改动只在 new_author 中使用,并不能更新到数据库
Author.objects.filter(pk=f.cleaned_data['pk']).update(f.cleaned_data)

太麻烦了!其实 Django 中已经有 update_or_create 方法已经实现了上述所有功能,可以避免这个 try ... except ... 判断实例是否已存在,我们来看这个例子

# forms.py 
class AuthorAddForm(forms.ModelForm):
# 确保 pk 不是必须字段
# 如果不传,自动识别为 None
pk = forms.IntegerField(required=False)
class Meta:
model = Author
fields = ['name', 'address']
def _post_clean(self):
super(forms.ModelForm, self)._post_clean()
# 不传 pk 的话表示需要新建一个条目
if not self.cleaned_data['pk']:
# 添加需要的 Field
self.cleaned_data['Origin'] = City.objects.get(Province='北京', City='北京')
# views.py 
class AuthorsView(LoginRequiredMixin, TemplateView):
template_name = 'authors.html'
def post(self, request):
form = AuthorAddForm(request.POST)
result = {}
if not form.is_valid():
return form.errors
# 如果 pk 不存在,为 None,那么 update_or_create 匹配失败,从而进入 create 流程
# 否则 pk 存在表单中,那么尝试匹配数据库,如果命中,进行 update 操作,否则进行 create 操作
Author, created = Author.objects.update_or_create(pk=form.cleaned_data['pk'], defaults=form.cleaned_data)
return Author.pk

其中 update_or_create 通过检测所有非 defaults 的字段,在上述例子中就是 id=form.cleaned_data['id'] 一项是否已存在于数据库而判断是用 update() 还是 create(),而不论是 update() 还是 create(),都会使用 cleaned_data 作为数据源来写入数据库。


你可能感兴趣的:(django)