在Django项目开发中,我们经常会遇到一类模型字段:它们的值不应该由用户通过表单输入,而是由系统在写入数据库时自动生成或采用默认值。例如记录的创建时间、流水号、或者由数据库触发器维护的字段。如果处理不当,这类字段要么在前端暴露被篡改,要么在表单提交时因缺失或无效值导致校验失败。本文将详细说明如何在Django表单中正确排除只读字段,并确保数据库默认值能够自动填充。

为什么不能直接把只读字段放进表单
很多初学者会尝试在模板中将字段设为disabled或readonly,以为这样就能实现只读效果。但实际上,HTML的disabled字段不会随表单提交,而readonly字段虽然会提交,但值仍可被用户通过浏览器开发者工具修改。在Django的ModelForm中,只要字段存在于fields列表里,就会参与清洗与校验流程。
更严重的问题是,如果模型该字段设置了default,但表单提交了空值或非法值,ModelForm在调用save时会用提交的值覆盖掉默认的意图。例如用户篡改了创建时间,系统就会存下错误时间。因此,最根本的做法是从表单层面彻底排除这些字段,让它们完全由模型或数据库控制。
使用exclude排除字段
Django的ModelForm提供了exclude属性,可以声明哪些模型字段不需要出现在表单中。被排除的字段不会生成表单控件,也不会参与is_valid的字段级校验。最常见的用法如下:
from django import forms
from .models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
# 只让用户填标题和内容
fields = ['title', 'content']
# 排除系统维护的字段
exclude = ['created_at', 'updated_at', 'author_id']
在上面的例子中,created_at和updated_at通常由数据库或模型自动维护,author_id可能在视图中根据登录用户赋值。由于它们不在fields中且被exclude显式排除,表单实例中根本不包含这些字段的BoundField,用户也无从提交它们。
需要注意的是,exclude和fields是互斥使用的。如果你已经用fields白名单精确列出了需要的字段,其实可以不用再写exclude。但在复杂模型中,使用exclude可以防止后续新增字段意外暴露到表单里,作为一种安全兜底策略是有价值的。
让数据库默认值自动生效
排除字段之后,下一个问题是:这些字段的值从哪来?答案就是依靠模型定义中的default参数,或者数据库自身的默认值约束。例如下面的模型定义:
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
# 创建时间由数据库默认填充
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
# 状态字段使用默认值
status = models.CharField(max_length=20, default='draft')
当ArticleForm调用save方法且未提供created_at时,由于表单已被排除该字段,ModelForm的save会创建一个不含created_at的模型实例,而Django ORM在执行INSERT语句时,数据库层面的auto_now_add或default就会生效。这样保证了每次插入都是系统时间,而不是用户可控值。
如果某些字段的默认值依赖于请求上下文,比如作者ID要根据当前登录用户填写,则可以在视图中手动赋值,而不是通过表单。示例代码如下:
def create_article(request):
if request.method == 'POST':
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False)
# 手动填充表单排除的字段
article.author_id = request.user.id
article.save()
return redirect('article_list')
else:
form = ArticleForm()
return render(request, 'article_form.html', {'form': form})
这里使用了commit=False先拿到未保存的模型对象,补充被排除的字段后再save。这种方式既保持了表单的简洁,又让关键字段由服务端控制,避免了任何客户端伪造的可能。
三种方案对比
为了更直观地理解,我们把常见的处理方式放在一起比较:
| 方案 | 是否提交字段 | 能否防篡改 | 默认值来源 |
|---|---|---|---|
| 前端disabled | 否 | 是(但不提交需后端补) | 需后端手动赋值 |
| 前端readonly | 是 | 否(可改DOM) | 用户可覆盖 |
| 表单exclude | 否 | 是 | 模型或数据库默认 |
从表中可以看出,只有exclude方案在表单层彻底切断了用户与字段的交互,同时配合模型default能实现真正的自动填充。前两种前端方案都只是交互层面的弱化,无法提供数据完整性保障。
此外,若项目使用了Django REST Framework之类的API层,同样应遵循类似逻辑:在Serializer中通过read_only_fields声明只读字段,并在perform_create中补齐上下文相关值,原理与ModelForm一致。
避坑与最佳实践
一个常见误区是认为在ModelForm里写widgets={'created_at': forms.HiddenInput}就安全了。隐藏输入框依然会提交值,且用户可以在HTML中修改隐藏域。这比readonly更隐蔽但同样危险。正确的边界原则是:凡是不希望用户触及的字段,一律不放进表单或序列化器的可写列表。
另一个实践是,当模型字段有数据库默认值但你在表单中误将其包含且未提供值时,Django会在clean阶段报Required错误。此时应检查Meta配置,确认该字段已被exclude或不在fields中。如果确实需要在管理后台由管理员偶尔修改,则可考虑使用单独的特权表单,而非混用同一表单类。
总结来说,Django中排除只读字段并自动填充数据库默认值的核心步骤为:在ModelForm中用exclude或精确fields控制可写字段;在模型中用default、auto_now_add等声明默认值;在视图中通过commit=False补充上下文字段。遵循这一实践,既能保证系统数据的准确性,也能有效防止越权写入。