Django 中 ModelForm 的使用


 

什么是 ModelForm

  • Model 在 Django 對應數據庫模型
    • 一個 Model 擁有多個 Model.Field
  • Form 在 Django 對應表單
    • 一個 Form 擁有多個 Form.Field

ModelForm 即基於 Model 的 Form,把 Model 中的 Field 根據下圖中的映射關系自動轉化為 Form 中的 Field。

為什么使用 ModelForm

利用 Model 生成 Form,提高 Model 復用性

如何使用 ModelForm

定義 ModelForm

舉一個書籍管理例子

# Model

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

這個 Model 中定義了兩個字段

  • title
    • 儲存書籍標題
    • 數據類型是 char
    • 最大長度 20
    • 數據庫唯一值限制,即不能儲存兩本相同標題的書
  • author
    • 儲存書籍的作者
    • 數據類型是外鍵,指向 Model Author

下面我們用 ModelForm 構建表單

# ModelForm

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

和下面手動構建表單的代碼等效

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

定制 ModelForm

很多情況下自動生成的 ModelForm 並不能滿足設計要求,下面我們來講一下如何定制

定制有兩種方式

  • Meta
    • 使用 Model 轉化的時候自定義轉化規則
  • 自定義字段
    • 定義額外的 Field,會覆蓋 Model 自動生成的 Field

Meta

ModelForm 是通過 Meta 來把 Model.Field 自動轉化為 Form.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_messages = {
'invalid' = 'invalid title'
}

# 自定義 widget
# 這里使用了長 80 列,寬 20 行的 textarea
widgets = {
'name': Textarea(attrs={'cols': 80, 'rows': 20}),
}

Meta 的缺點是不能修改字段的 validators,如果需要自定義 validators,需要在 Meta 外部重新定義一個同名 Field 來覆蓋自動生成的 Field

在 Form 中另外定義 Field

這是 Form 中定義 Field 的通用方法,在 ModelForm 中它有兩個作用

  • 補充 Model 沒有的 Field 到 Form
  • 覆蓋 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, 'alice'), (2, 'bob'),), 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)
article = Article.objects.get(pk=1)
author = Author.objects.first()

form = ArticleForm(request.POST, instance=article, initial={'author': author})
# form 綁定到 article 實例了
# 初始化表單的時候,author 字段的初始值為 author

if form.is_valid():
form.save()
  • instance
    • 給 ModelForm 初始化 Model 實例,后續的操作都作用在這個實例上
  • initial
    • 給 ModelForm 初始值
    • 如果和 instance 同時被定義,同名 field 的值覆蓋 instance 中的值

數據加載的先后順序為 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 注冊的 validators 做單個 Field 的校驗 (比如 title 字段就會校驗是否超出最大允許長度 20) 其中在 Field.clean() 執行過后提供了鈎子 clean_[field_name],可以自定義該 function 來注冊自己的校驗方法。
  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() 方法。

定義一個 Form 來新建、更新實例

通常的步驟分為如下幾步

  • 檢測該對象是否已在數據庫
    • 如果已存在,那么手動獲取該實例,然后更改相關 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 作為數據源來寫入數據庫。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM