Django Form 组件详解:从表单生成到 ModelForm 数据校验

一、为什么需要 Form 组件

在没有 Form 组件时,我们需要手写 HTML 表单、读取 request.POST,并为每个字段编写长度、格式和必填校验。字段一多,模板和视图就会重复很多逻辑。

Django 的 Form 组件把"字段定义、HTML 生成、数据清洗、错误提示"集中到一个 Python 类中。它既可以生成表单控件,也可以在服务端统一校验用户提交的数据。需要注意:浏览器端的校验只能改善交互体验,真正可靠的校验必须在服务端执行。

下面是手写 HTML 表单的例子。它能展示页面,但不会自动完成 Django 的数据校验。

html 复制代码
<form action="" method="post">
  <div class="form-group">
    <label for="inputUsername">用户名</label>
    <input type="text" name="username" id="inputUsername">
  </div>
  <div class="form-group">
    <label for="inputPassword">密码</label>
    <input type="password" name="password" id="inputPassword">
  </div>
  <button type="submit">提交</button>
</form>

二、创建并使用一个 Form

通常将表单类放在应用的 forms.py 中。forms.Form 不会自动创建数据库表,它只描述当前页面需要的字段及校验规则。

1. 定义表单类

python 复制代码
# users/forms.py
from django import forms


class RegisterForm(forms.Form):
    username = forms.CharField(max_length=8, min_length=3, label="用户名")
    password = forms.CharField(
        max_length=6,
        min_length=2,
        label="密码",
        widget=forms.PasswordInput,
    )
    email = forms.EmailField(label="邮箱")

代码中的每个字段都会对应一个 HTML 控件。max_lengthmin_length 限制的是用户提交到表单中的字符长度;它们与模型字段的数据库长度约束不是同一个概念。

2. 在视图中绑定数据

python 复制代码
# users/views.py
from django.shortcuts import render
from django.views import View

from .forms import RegisterForm


class RegisterView(View):
    def get(self, request, *args, **kwargs):
        # 未提交数据时创建未绑定表单,用于展示空表单。
        form = RegisterForm()
        return render(request, "register.html", {"form": form})

    def post(self, request, *args, **kwargs):
        # 传入 request.POST 后,表单成为"已绑定"状态,可以执行校验。
        form = RegisterForm(request.POST)
        if form.is_valid():
            # cleaned_data 是经过类型转换和校验后的可信数据。
            data = form.cleaned_data
            # 在这里执行创建用户等业务操作。
            return render(request, "register_success.html", {"data": data})

        # 校验失败时把同一个 form 返回模板,用户输入和错误信息都会保留。
        return render(request, "register.html", {"form": form})

is_valid() 会触发所有字段的内置校验和自定义校验。只有返回 True 时才应该使用 cleaned_data;失败时应将表单重新渲染,让用户看到具体错误。

3. 在模板中输出表单

html 复制代码
<form action="" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <button type="submit">提交</button>
</form>

POST 表单通常需要 {% csrf_token %},否则 Django 的 CSRF 中间件会拒绝请求。{``{ form.as_p }} 只是 Django 提供的一种快速布局方式,实际项目也可以逐字段编写模板,以获得更精细的样式和错误展示。

三、常用字段参数与控件

字段参数决定了表单的显示方式和校验行为,常用参数如下:

python 复制代码
from django import forms


class ProfileForm(forms.Form):
    username = forms.CharField(
        max_length=20,
        min_length=3,
        required=True,              # 是否必填,默认 True
        initial="dream",           # 未绑定表单的初始值
        label="用户名",             # 标签文字
        label_suffix=":",           # 标签后缀
        help_text="3~20 个字符",
        error_messages={
            "required": "用户名不能为空",
            "max_length": "用户名不能超过 20 个字符",
        },
        widget=forms.TextInput(attrs={
            "class": "form-control",
            "placeholder": "请输入用户名",
        }),
    )
    password = forms.CharField(
        label="密码",
        widget=forms.PasswordInput(attrs={"class": "form-control"}),
    )
    gender = forms.ChoiceField(
        label="性别",
        choices=(("0", "女"), ("1", "男")),
        widget=forms.RadioSelect,
    )
    city = forms.ChoiceField(
        label="城市",
        choices=(("sh", "上海"), ("bj", "北京")),
    )
    hobbies = forms.MultipleChoiceField(
        label="爱好",
        choices=(("read", "阅读"), ("sport", "运动")),
        widget=forms.CheckboxSelectMultiple,
        required=False,
    )

widget 负责生成 HTML 控件,attrs 可以为控件添加 classplaceholder 等属性。ChoiceField 适合单选或下拉框,MultipleChoiceField 适合多选。提交的数据通常是字符串,业务代码需要根据实际需求进行类型转换。

常见字段速查:

text 复制代码
CharField                 文本,支持 max_length、min_length
IntegerField              整数,支持 min_value、max_value
FloatField / DecimalField 小数;DecimalField 可限制总位数和小数位
DateField / TimeField     日期或时间
EmailField                邮箱格式
URLField                  URL 格式
BooleanField              布尔值
ChoiceField               单选或下拉选项
MultipleChoiceField       多选选项
FileField                 文件上传
ImageField                图片上传,需要 Pillow
ModelChoiceField          从 QuerySet 中选择一个模型对象
ModelMultipleChoiceField  从 QuerySet 中选择多个模型对象

文件上传必须同时满足两个条件:HTML 表单设置 enctype="multipart/form-data",视图绑定 request.FILES

python 复制代码
class UploadForm(forms.Form):
    file = forms.FileField()


form = UploadForm(request.POST, request.FILES)

四、Form 的几种渲染方式

Django 可以快速把整个表单渲染成不同的 HTML 结构:

html 复制代码
{{ form }}          <!-- 默认结构,通常是 div 或段落结构 -->
{{ form.as_p }}     <!-- 每个字段放入 p 标签 -->
{{ form.as_ul }}    <!-- 每个字段放入 li,需要 ul/ol 包裹 -->
{{ form.as_table }} <!-- 每个字段放入 tr,需要 table 包裹 -->

需要完全控制布局时,可以遍历字段。这样既能输出控件,也能分别处理标签、帮助文本和错误信息。

html 复制代码
{% for field in form %}
  <div class="field">
    {{ field.label_tag }}
    {{ field }}
    {% if field.help_text %}<small>{{ field.help_text }}</small>{% endif %}
    {% for error in field.errors %}<p class="error">{{ error }}</p>{% endfor %}
  </div>
{% endfor %}

如果需要暂时关闭浏览器原生校验,可以在 <form> 上添加 novalidate。这不会关闭 Django 的服务端校验:

html 复制代码
<form method="post" novalidate>

五、钩子函数:补充业务校验

字段的 max_lengthEmailField 等规则只能解决通用问题。对于"用户名必须以 nb_ 开头""两次密码必须一致"等业务规则,可以使用清洗钩子:

  • clean_字段名():局部钩子,只校验一个字段。
  • clean():全局钩子,适合比较多个字段。

不要把 onBluronSubmit 等前端事件称为 Django Form 钩子;它们属于 JavaScript 交互,与服务端的 clean_*clean 不同。

python 复制代码
from django import forms


class LoginForm(forms.Form):
    username = forms.CharField(max_length=20, label="用户名")
    password = forms.CharField(widget=forms.PasswordInput, label="密码")
    confirm_password = forms.CharField(
        widget=forms.PasswordInput,
        label="确认密码",
    )

    def clean_username(self):
        username = self.cleaned_data["username"]
        if not username.startswith("nb_"):
            raise forms.ValidationError("用户名必须以 nb_ 开头")
        return username

    def clean(self):
        cleaned_data = super().clean()
        password = cleaned_data.get("password")
        confirm_password = cleaned_data.get("confirm_password")
        if password and confirm_password and password != confirm_password:
            self.add_error("confirm_password", "两次密码不一致")
        return cleaned_data

局部钩子返回字段值,才能继续把它放入 cleaned_data。全局钩子应先调用 super().clean(),这样才能拿到已经通过字段校验的数据。字段不存在或字段校验失败时,使用 .get() 可以避免额外的 KeyError

六、ModelForm:让表单复用模型定义

ModelFormForm 的扩展。它可以根据模型字段自动生成表单字段,并在校验通过后直接保存模型,适合新增和编辑数据的场景。模型字段的数据库约束仍然需要正确设置,表单校验不能替代数据库约束。

1. 模型与 ModelForm

python 复制代码
# users/models.py
from django.db import models


class User(models.Model):
    username = models.CharField(max_length=32)
    password = models.CharField(max_length=128)
python 复制代码
# users/forms.py
from django import forms
from .models import User


class UserModelForm(forms.ModelForm):
    # 手动声明的字段会覆盖模型自动生成的同名字段。
    username = forms.CharField(
        max_length=20,
        min_length=3,
        label="用户名",
        widget=forms.TextInput(attrs={"class": "form-control"}),
    )

    class Meta:
        model = User
        # fields 与 exclude 二选一,不能同时配置。
        fields = ["username", "password"]

fields = "__all__" 会包含模型的全部字段,但不建议在用户注册等敏感场景中滥用,否则模型新增字段后可能被意外暴露。更稳妥的做法是显式列出允许提交的字段。

2. 校验、保存与编辑

python 复制代码
class UserCreateView(View):
    def post(self, request, *args, **kwargs):
        form = UserModelForm(request.POST)
        if form.is_valid():
            user = form.save()  # 校验后的数据写入数据库
            return render(request, "success.html", {"user": user})
        return render(request, "user_form.html", {"form": form})


# 编辑已有对象时传入 instance;提交后 form.save() 会执行更新而不是新增。
form = UserModelForm(request.POST or None, instance=user)

如果需要先补充字段或执行额外业务逻辑,可以使用 commit=False,完成修改后再保存:

python 复制代码
from django.contrib.auth.hashers import make_password


user = form.save(commit=False)
user.password = make_password(user.password)
user.save()

七、总结

  1. Form 适合独立于数据库的表单;ModelForm 适合直接对应模型的数据录入和编辑。
  2. 未绑定表单用于展示,绑定 request.POST 后才能校验;文件上传还要绑定 request.FILES
  3. is_valid() 成功后再读取 cleaned_data,失败时把原表单返回模板以显示错误。
  4. clean_字段名() 用于单字段规则,clean() 用于跨字段规则。
  5. 快速渲染适合简单页面,复杂页面应逐字段渲染并明确展示错误信息。
相关推荐
梦想不只是梦与想2 小时前
MySQL 数据库(二):数据类型
数据库·mysql·数据类型
野生技术架构师2 小时前
Redis 和 MySQL 如何保证数据一致性?先更新数据库还是先删缓存,延迟双删、MQ、Canal 一次讲透
数据库·redis·缓存
鸽芷咕2 小时前
SQL Server数据迁移到金仓数据库复盘:从怕性能翻车,到TPS提升60%
数据库
weixin_BYSJ19872 小时前
springboot技能与工具共享小程序---附源码29657
java·javascript·spring boot·python·小程序·django·php
HAYDENR3 小时前
数据库如何做性能优化?数据库性能调优有哪些常见注意事项?
数据库·性能优化
weixin_BYSJ19873 小时前
flask民族服饰饰品商城小程序---附源码37399
java·javascript·spring boot·python·小程序·django·php
Light Gao3 小时前
企业级灰度发布技术方案
网络·数据库·oracle
一水3 小时前
AI 时代审查思维:审查第一篇
java·jvm·数据库·spring
赵广陆3 小时前
企业实战:Milvues向量数据库实践
数据库·pycharm·langchain