表單與資料驗證¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解為什麼需要表單驗證
- 手寫 HTML 表單並在 view 中手動驗證資料
- 使用 Django Form 類別處理驗證邏輯
- 使用 Form 類別渲染表單 HTML
- 使用 ModelForm 簡化表單建立
- 掌握表單處理的最佳實踐
為什麼需要表單驗證?¶
在網頁開發中,使用者輸入是不可信任的。
常見的問題¶
| 問題 | 範例 | 可能造成的後果 |
|---|---|---|
| 空值 | 標題欄位留空 | 建立無意義的資料 |
| 格式錯誤 | Email 格式不正確 | 無法聯絡使用者 |
| 過長的內容 | 標題超過 200 字元 | 資料庫錯誤 |
| 惡意輸入 | SQL Injection、XSS | 安全漏洞 |
| 不合邏輯的值 | 負數的價格 | 業務邏輯錯誤 |
永遠不要信任使用者輸入
即使前端有驗證,後端也必須再次驗證:
- 使用者可以停用 JavaScript
- 可以透過開發者工具修改 HTML
- 可以直接發送 HTTP 請求繞過前端
- 前端驗證只是為了提升使用者體驗
後端驗證是必須的,前端驗證是可選的。
情境:建立文章功能¶
讓我們實作一個建立文章的功能,從最基本的方式開始,逐步優化。
需求分析¶
建立文章時需要:
- 必填欄位:標題、內容
- 選填欄位:作者
- 驗證規則:
- 標題不能空白
- 標題最多 200 字元
- 內容不能空白
方法一:手寫表單 + 手動驗證¶
讓我們從最基礎的方式開始:手寫 HTML 表單,並在 view 中手動處理驗證。
建立 View¶
新增 article_create view 到 blog/views.py:
from django.shortcuts import get_object_or_404, redirect, render
from blog.models import Article, Author
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags")
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
authors = Author.objects.all() # (1)!
errors = {}
if request.method == "POST":
title = request.POST.get("title", "").strip() # (2)!
content = request.POST.get("content", "").strip()
author_id = request.POST.get("author")
if not title: # (3)!
errors["title"] = "標題不能空白"
elif len(title) > 200:
errors["title"] = "標題最多 200 字元"
if not content:
errors["content"] = "內容不能空白"
if not errors: # (4)!
article = Article.objects.create(
title=title,
content=content,
author_id=author_id if author_id else None,
)
return redirect("blog:article_detail", article_id=article.id)
return render(
request,
"blog/article_create.html",
{
"authors": authors,
"errors": errors,
"title": request.POST.get("title", ""),
"content": request.POST.get("content", ""),
"author_id": request.POST.get("author", ""),
},
)
- 準備作者列表供下拉選單使用
- 取得表單資料
- 驗證資料
- 如果沒有錯誤,儲存資料
程式碼解析
這個 view 處理兩種 HTTP 請求:
-
GET 請求:顯示空白表單
- 準備作者列表
- 渲染表單 template
-
POST 請求:處理表單提交
- 取得表單資料
- 手動驗證每個欄位
- 如果有錯誤,顯示錯誤訊息並保留使用者輸入
- 如果沒有錯誤,建立文章並重導向到文章詳情頁面
建立 Template¶
建立 blog/templates/blog/article_create.html:
{% extends "blog/base.html" %}
{% block title %}
建立文章 - Django 大冒險
{% endblock title %}
{% block blog_content %}
<div class="card">
<div class="card-body">
<h2 class="card-title">
建立文章
</h2>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="title" class="form-label">標題</label>
<input type="text"
class="form-control{% if errors.title %} is-invalid{% endif %}"
id="title"
name="title"
value="{{ title }}"
required />
{% if errors.title %}
<div class="invalid-feedback">
{{ errors.title }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="content" class="form-label">內容</label>
<textarea class="form-control{% if errors.content %} is-invalid{% endif %}"
id="content"
name="content"
rows="10"
required>{{ content }}</textarea>
{% if errors.content %}
<div class="invalid-feedback">
{{ errors.content }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="author" class="form-label">作者(選填)</label>
<select class="form-select" id="author" name="author">
<option value="">未指定</option>
{% for author in authors %}
<option value="{{ author.id }}"
{% if author_id|stringformat:"s" == author.id|stringformat:"s" %}selected{% endif %}>
{{ author.name }}
</option>
{% endfor %}
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
建立文章
</button>
<a href="{% url 'blog:article_list' %}" class="btn btn-secondary">
取消
</a>
</div>
</form>
</div>
</div>
{% endblock blog_content %}
Template 重點
{% csrf_token %}:防止 CSRF 攻擊的 token,Django 必須- 錯誤顯示:使用
errors字典顯示驗證錯誤 - 保留輸入:驗證失敗時,保留使用者輸入的值
- Bootstrap 樣式:使用
is-invalid類別標示錯誤欄位
設定 URL¶
在 blog/urls.py 加入新的 URL pattern:
from django.urls import path
from blog import views
app_name = "blog"
urlpatterns = [
path("articles/", views.article_list, name="article_list"),
path("articles/create/", views.article_create, name="article_create"),
path("articles/<int:article_id>/", views.article_detail, name="article_detail"),
]
測試功能¶
啟動開發伺服器:
訪問 http://127.0.0.1:8000/blog/articles/create/
試試看:
- 提交空白表單:應該看到「標題不能空白」和「內容不能空白」的錯誤
- 輸入超長標題:應該看到「標題最多 200 字元」的錯誤
- 正確填寫表單:應該成功建立文章並重導向
問題分析¶
手動驗證的方式可以運作,但有很多問題:
手動驗證的缺點
- 重複的程式碼:每個表單都要寫類似的驗證邏輯
- 容易出錯:可能忘記驗證某些欄位
- 難以維護:驗證規則分散在各個 view 中
- 測試困難:需要測試整個 view,無法單獨測試驗證邏輯
- 不一致:不同開發者可能寫出不同的驗證方式
Django 提供了更好的解決方案:Form 類別。
方法二:使用 Django Form 類別¶
Django 的 Form 類別可以幫我們處理驗證邏輯。
建立 Form 類別¶
建立 blog/forms.py:
from django import forms
class ArticleForm(forms.Form):
title = forms.CharField(
max_length=200,
required=True,
error_messages={
"required": "標題不能空白",
"max_length": "標題最多 %(limit_value)d 字元",
},
)
content = forms.CharField(
widget=forms.Textarea,
required=True,
error_messages={
"required": "內容不能空白",
},
)
author = forms.IntegerField(
required=False,
)
Form 欄位說明
-
CharField:文字欄位
max_length:最大長度限制required:是否必填error_messages:自訂錯誤訊息
-
Textarea widget:多行文字輸入框
widget=forms.Textarea讓 content 渲染成 textarea
-
IntegerField:整數欄位
required=False:選填欄位
Django 會自動驗證這些規則!
重構 View¶
修改 blog/views.py,使用 Form 類別:
from django.shortcuts import get_object_or_404, redirect, render
from blog.forms import ArticleForm
from blog.models import Article, Author
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags")
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
authors = Author.objects.all()
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = Article.objects.create( # (1)!
title=form.cleaned_data["title"],
content=form.cleaned_data["content"],
author_id=form.cleaned_data["author"],
)
return redirect("blog:article_detail", article_id=article.id)
else:
form = ArticleForm()
return render(
request,
"blog/article_create.html",
{
"form": form,
"authors": authors,
},
)
- 取得驗證後的資料
程式碼變簡潔了!
對比手動驗證的版本:
- 減少了 20+ 行程式碼
- 不需要手動取得每個欄位:
form.cleaned_data自動提供驗證後的資料 - 不需要手動驗證:
form.is_valid()自動執行所有驗證 - 不需要手動處理錯誤:Form 會自動記錄錯誤訊息
更新 Template¶
修改 blog/templates/blog/article_create.html,使用 Form 物件:
{% extends "blog/base.html" %}
{% block title %}
建立文章 - Django 大冒險
{% endblock title %}
{% block blog_content %}
<div class="card">
<div class="card-body">
<h2 class="card-title">
建立文章
</h2>
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="{{ form.title.id_for_label }}" class="form-label">標題</label>
<input type="text"
class="form-control{% if form.title.errors %} is-invalid{% endif %}"
id="{{ form.title.id_for_label }}"
name="{{ form.title.name }}"
value="{{ form.title.value|default:'' }}"
required />
{% if form.title.errors %}
<div class="invalid-feedback">
{{ form.title.errors.0 }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.content.id_for_label }}" class="form-label">內容</label>
<textarea class="form-control{% if form.content.errors %} is-invalid{% endif %}"
id="{{ form.content.id_for_label }}"
name="{{ form.content.name }}"
rows="10"
required>{{ form.content.value|default:'' }}</textarea>
{% if form.content.errors %}
<div class="invalid-feedback">
{{ form.content.errors.0 }}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="{{ form.author.id_for_label }}" class="form-label">作者(選填)</label>
<select class="form-select"
id="{{ form.author.id_for_label }}"
name="{{ form.author.name }}">
<option value="">未指定</option>
{% for author in authors %}
<option value="{{ author.id }}"
{% if form.author.value == author.id %}selected{% endif %}>
{{ author.name }}
</option>
{% endfor %}
</select>
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
建立文章
</button>
<a href="{% url 'blog:article_list' %}" class="btn btn-secondary">
取消
</a>
</div>
</form>
</div>
</div>
{% endblock blog_content %}
Form 物件的屬性
form.title.id_for_label:欄位的 HTML idform.title.name:欄位的 HTML nameform.title.value:欄位的值(保留使用者輸入)form.title.errors:欄位的錯誤訊息列表
測試功能¶
重新整理頁面,測試驗證功能:
- 提交空白表單:應該看到自訂的錯誤訊息
- 輸入超長標題:應該看到「標題最多 200 字元」
- 正確提交:成功建立文章
使用 Form 的優勢
✅ 驗證邏輯集中:所有驗證規則在 Form 類別中
✅ 可重用:可以在不同 view 中使用同一個 Form
✅ 可測試:可以單獨測試 Form 的驗證邏輯
✅ 自動類型轉換:cleaned_data 中的資料已經是正確的類型
✅ 安全:Django 會自動處理 XSS 等安全問題
方法三:使用 Form 渲染表單¶
目前我們還在手寫 HTML,Django Form 可以幫我們生成 HTML。
簡化 Template¶
修改 blog/templates/blog/article_create.html:
{% extends "blog/base.html" %}
{% block title %}
建立文章 - Django 大冒險
{% endblock title %}
{% block blog_content %}
<div class="card">
<div class="card-body">
<h2 class="card-title">
建立文章
</h2>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
建立文章
</button>
<a href="{% url 'blog:article_list' %}" class="btn btn-secondary">
取消
</a>
</div>
</form>
</div>
</div>
{% endblock blog_content %}
只要一行!
{{ form.as_p }} 會自動生成:
- 所有欄位的 label
- 所有欄位的 input
- 所有錯誤訊息
- 包裹在
<p>標籤中
從 60+ 行縮減到 1 行!
Form 渲染方法¶
Django 提供了幾種渲染方式:
| 方法 | 輸出格式 | 說明 |
|---|---|---|
{{ form.as_p }} |
<p> 標籤 |
每個欄位包裹在段落中 |
{{ form.as_table }} |
<tr> 標籤 |
適合用在表格中 |
{{ form.as_div }} |
<div> 標籤 |
Django 4.1+ 可用 |
{{ form.as_ul }} |
<li> 標籤 |
適合用在列表中 |
測試渲染結果¶
重新整理頁面,你會看到表單已經自動生成,包括:
- Label 標籤
- Input 欄位
- 錯誤訊息
樣式可能不理想
{{ form.as_p }} 生成的 HTML 是最基本的,沒有 Bootstrap 樣式。
如果需要客製化樣式,有幾種選擇:
- 手動渲染欄位(前面的方法)
- 使用 django-widget-tweaks:在 template 中加入 CSS 類別
- 使用 django-crispy-forms:自動套用 Bootstrap 樣式
- 自訂 Form widget:在 Form 類別中設定樣式
我們會在後續章節介紹如何整合 Bootstrap。
處理作者下拉選單¶
目前的實作有個問題:作者欄位顯示的是 ID,不是下拉選單。
讓我們修改 Form 類別,使用 ChoiceField:
from django import forms
from blog.models import Author
class ArticleForm(forms.Form):
title = forms.CharField(
max_length=200,
required=True,
label="標題",
error_messages={
"required": "標題不能空白",
"max_length": "標題最多 %(limit_value)d 字元",
},
)
content = forms.CharField(
widget=forms.Textarea,
required=True,
label="內容",
error_messages={
"required": "內容不能空白",
},
)
author = forms.ChoiceField(
choices=[],
required=False,
label="作者",
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["author"].choices = [("", "未指定")] + [ # (1)!
(author.id, author.name) for author in Author.objects.all()
]
- 動態載入作者選項
程式碼解析
-
ChoiceField:下拉選單欄位
choices:選項列表,格式為[(value, label), ...]- 初始設定為空列表
-
__init__方法:Form 的建構子- 在建立 Form 時動態載入作者選項
- 這樣可以確保每次都取得最新的作者列表
-
choices 格式:
("", "未指定"):空值選項(author.id, author.name):作者 ID 和名稱
-
label 設定: 設定顯示的 label
更新 View¶
因為現在不需要手動傳遞 authors,可以簡化 view:
from django.shortcuts import get_object_or_404, redirect, render
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags")
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = Article.objects.create(
title=form.cleaned_data["title"],
content=form.cleaned_data["content"],
author_id=form.cleaned_data["author"] or None, # (1)!
)
return redirect("blog:article_detail", article_id=article.id)
else:
form = ArticleForm()
return render(
request,
"blog/article_create.html",
{"form": form},
)
- 將空字串轉成
None
View 變得更簡潔
- 不需要手動查詢 authors
- 不需要傳遞 authors 到 template
- Form 自己處理所有邏輯
測試功能¶
重新整理頁面,現在應該可以看到:
- 完整的表單欄位
- 作者下拉選單
- 驗證錯誤訊息
方法四:使用 ModelForm¶
目前的 Form 類別和 Article Model 有很多重複的定義。Django 提供了 ModelForm,可以自動從 Model 生成 Form。
建立 ModelForm¶
修改 blog/forms.py:
from django import forms
from blog.models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "content", "author"]
labels = {
"title": "標題",
"content": "內容",
"author": "作者",
}
error_messages = {
"title": {
"required": "標題不能空白",
"max_length": "標題最多 %(limit_value)d 字元",
},
"content": {
"required": "內容不能空白",
},
}
widgets = {
"content": forms.Textarea(attrs={"rows": 10}),
}
ModelForm 的優勢
相比手動定義 Form:
- 自動欄位:根據 Model 自動生成欄位類型
- 自動驗證:根據 Model 的限制自動驗證(如 max_length)
- 簡化建立:可以直接使用
form.save()建立物件 - 減少重複:不需要重複定義欄位
Meta 類別說明
- model:對應的 Model
- fields:要包含的欄位列表
- 可以用
"__all__"包含所有欄位 - 可以用
exclude排除特定欄位
- 可以用
- labels:自訂欄位標籤
- error_messages:自訂錯誤訊息
- widgets:自訂欄位的 HTML widget
簡化 View¶
使用 ModelForm 的 save() 方法:
from django.shortcuts import get_object_or_404, redirect, render
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags")
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save()
return redirect("blog:article_detail", article_id=article.id)
else:
form = ArticleForm()
return render(
request,
"blog/article_create.html",
{"form": form},
)
更簡潔了!
form.save() 會自動:
- 從
cleaned_data取得資料 - 建立 Article 物件
- 儲存到資料庫
- 回傳建立的物件
不需要手動呼叫 Article.objects.create()!
測試功能¶
重新整理頁面,功能應該完全相同,但程式碼更簡潔了。
新增導覽連結¶
讓我們在文章列表頁面加入「建立文章」按鈕。
修改 blog/templates/blog/article_list.html:
{% extends "blog/base.html" %}
{% block title %}
文章列表 - Django 大冒險
{% endblock title %}
{% block blog_content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h2>
文章列表
</h2>
<a href="{% url 'blog:article_create' %}" class="btn btn-primary">
建立文章
</a>
</div>
<div class="row">
{% for article in articles %}
{% include "blog/components/article_card.html" %}
{% empty %}
<div class="col-12">
<div class="alert alert-info">
目前沒有文章
</div>
</div>
{% endfor %}
</div>
{% endblock blog_content %}
現在在文章列表頁面就能看到「建立文章」按鈕了!
表單處理的最佳實踐¶
1. POST-Redirect-GET 模式¶
永遠在 POST 後重導向
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save()
return redirect("blog:article_detail", article_id=article.id) # 重導向
為什麼要重導向?
- 避免重複提交:使用者重新整理頁面不會重複建立資料
- 改善使用者體驗:顯示成功訊息的頁面
- 符合 HTTP 語意:POST 用於修改資料,GET 用於查看資料
2. 永遠驗證資料¶
後端必須驗證
# ❌ 錯誤:直接使用 request.POST
Article.objects.create(
title=request.POST.get("title"),
content=request.POST.get("content"),
)
# ✅ 正確:透過 Form 驗證
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save()
永遠不要直接使用 request.POST 的資料!
3. 使用 cleaned_data¶
使用驗證後的資料
# ❌ 錯誤:使用原始資料
title = request.POST.get("title")
# ✅ 正確:使用 cleaned_data
title = form.cleaned_data["title"]
cleaned_data 的資料:
- 已經驗證
- 已經轉換成正確的類型
- 已經去除空白
- 已經經過安全處理
4. 選擇合適的 Form 類別¶
| 情境 | 使用 | 理由 |
|---|---|---|
| 直接對應 Model | ModelForm | 最簡單,減少重複 |
| 不對應 Model | Form | 例如登入表單、搜尋表單 |
5. 自訂驗證¶
如果需要自訂驗證邏輯,可以覆寫 clean_<field_name> 方法:
from django import forms
from blog.models import Article
class ArticleForm(forms.ModelForm):
class Meta:
model = Article
fields = ["title", "content", "author"]
labels = {
"title": "標題",
"content": "內容",
"author": "作者",
}
error_messages = {
"title": {
"required": "標題不能空白",
"max_length": "標題最多 %(limit_value)d 字元",
},
"content": {
"required": "內容不能空白",
},
}
widgets = {
"content": forms.Textarea(attrs={"rows": 10}),
}
def clean_title(self):
title = self.cleaned_data["title"]
if "測試" in title:
error_message = "標題不能包含「測試」"
raise forms.ValidationError(error_message)
return title
自訂驗證方法
clean_<field_name>():驗證單一欄位clean():驗證多個欄位的關聯性
例如:
6. 簡化 View 寫法¶
可以使用 request.POST or None 來簡化 view 的程式碼:
from django.shortcuts import get_object_or_404, redirect, render
from blog.forms import ArticleForm
from blog.models import Article
def article_list(request):
articles = Article.objects.select_related("author").prefetch_related("tags")
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(
Article.objects.select_related("author").prefetch_related("tags"),
id=article_id,
)
return render(request, "blog/article_detail.html", {"article": article})
def article_create(request):
form = ArticleForm(request.POST or None)
if form.is_valid():
article = form.save()
return redirect("blog:article_detail", article_id=article.id)
return render(request, "blog/article_create.html", {"form": form})
為什麼這樣可以運作?
原本的寫法:
if request.method == "POST":
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save()
return redirect("blog:article_detail", article_id=article.id)
else:
form = ArticleForm()
簡化後的寫法:
form = ArticleForm(request.POST or None)
if form.is_valid():
article = form.save()
return redirect("blog:article_detail", article_id=article.id)
運作原理:
-
GET 請求:
request.POST是空的 QueryDict(類似空字典)- 空 QueryDict 在 Python 中被視為
False request.POST or None回傳NoneArticleForm(None)等同於ArticleForm(),建立空白表單
-
POST 請求:
request.POST包含資料- 有資料的 QueryDict 被視為
True request.POST or None回傳request.POSTArticleForm(request.POST)建立包含資料的表單
-
is_valid() 的行為:
- 空白表單(GET):
is_valid()回傳False,不會進入處理邏輯 - 包含資料的表單(POST):執行驗證,根據結果回傳
True或False
- 空白表單(GET):
優點與缺點
優點:
- ✅ 程式碼更簡潔(減少 5-6 行)
- ✅ 減少縮排層級
- ✅ 更符合 Python 的風格
缺點:
- ❌ 對初學者可能不夠直觀
- ❌ 如果需要在 POST/GET 執行不同邏輯,還是需要判斷
建議:
- 初學時使用明確的
if request.method == "POST"判斷 - 熟悉後可以改用
request.POST or None簡化程式碼 - 如果邏輯複雜,仍然使用明確判斷比較清楚
常見問題¶
ModelForm 和 Form 有什麼差別?¶
| 特性 | Form | ModelForm |
|---|---|---|
| 基礎類別 | forms.Form |
forms.ModelForm |
| 用途 | 任何表單 | 對應 Model 的表單 |
| 欄位定義 | 手動定義 | 自動從 Model 生成 |
| 儲存 | 手動處理 | form.save() |
| 適用場景 | 登入、搜尋等 | 建立、編輯 Model 物件 |
form.save() 做了什麼?¶
form.save() 會:
- 從
cleaned_data取得資料 - 建立或更新 Model 物件
- 儲存到資料庫
- 回傳物件
也可以使用 commit=False 來延遲儲存:
- 建立物件但不儲存到資料庫
- 修改其他欄位
- 手動儲存到資料庫
處理多對多關係時的注意事項
如果表單包含多對多欄位(如 tags),使用 commit=False 時需要額外處理:
form = ArticleForm(request.POST)
if form.is_valid():
article = form.save(commit=False) # (1)!
article.is_published = True # (2)!
article.save() # (3)!
form.save_m2m() # (4)!
- 建立物件但不儲存到資料庫
- 修改其他欄位
- 先儲存物件到資料庫,取得 ID
- 儲存多對多關係
為什麼需要 save_m2m()?
- 多對多關係需要透過中介表連接
- 中介表需要兩個物件的 ID
- 使用
commit=False時,物件還沒有 ID - 必須先
save()取得 ID,再呼叫save_m2m()儲存關聯
完整流程:
form.save(commit=False)- 建立物件但不儲存- 修改額外欄位
article.save()- 儲存物件到資料庫(取得 ID)form.save_m2m()- 儲存多對多關係
可以不用 Form 嗎?¶
技術上可以,但強烈不建議:
- 失去自動驗證
- 失去自動類型轉換
- 失去安全保護
- 程式碼難以維護
Form 是 Django 的核心功能,應該總是使用它。
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解為什麼需要表單驗證
- 手寫 HTML 表單並在 view 中手動驗證資料
- 使用 Django Form 類別處理驗證邏輯
- 使用 Form 類別渲染表單 HTML
- 使用 ModelForm 簡化表單建立
- 掌握表單處理的最佳實踐
表單處理的演進
回顧我們的學習歷程:
- 手動處理:完全控制,但重複且容易出錯
- Form 類別:集中驗證邏輯,可重用
- Form 渲染:自動生成 HTML,減少重複
- ModelForm:進一步簡化,與 Model 緊密整合
從 60+ 行程式碼優化到不到 10 行,這就是 Django 的威力!
記住關鍵原則:
- 永遠使用 Form:不要手動驗證
- 優先使用 ModelForm:如果對應 Model
- POST 後重導向:避免重複提交
- 使用 cleaned_data:不要直接使用 request.POST
- 後端必須驗證:不要依賴前端驗證
掌握了 Django Form,你就掌握了安全、可維護的資料處理方式!