Django模型驗(yàn)證器介紹與源碼分析
前言
在Django的模型字段參數(shù)中,有一個(gè)參數(shù)叫做validators,這個(gè)參數(shù)是用來(lái)指定當(dāng)前字段需要使用的驗(yàn)證器,也就是對(duì)字段數(shù)據(jù)的合法性進(jìn)行驗(yàn)證,比如大小、類(lèi)型等。
Django的驗(yàn)證器可以分為模型相關(guān)的驗(yàn)證器和表單相關(guān)的驗(yàn)證器,它們基本類(lèi)似,但在使用上有區(qū)別。
本文討論的是模型相關(guān)的驗(yàn)證器。
一、自定義驗(yàn)證器
一個(gè)驗(yàn)證器其實(shí)就是一個(gè)可調(diào)用的對(duì)象(函數(shù)或類(lèi)),接收一個(gè)初始輸入值作為參數(shù),對(duì)這個(gè)值進(jìn)行一系列邏輯判斷,如果不滿(mǎn)足某些規(guī)則或者條件,則表示驗(yàn)證不通過(guò),拋出一個(gè)ValidationError異常。如果滿(mǎn)足條件則通過(guò)驗(yàn)證,不返回任何內(nèi)容(也就是默認(rèn)的return None),可以繼續(xù)下一步。
驗(yàn)證器具有重要作用,可以被重用在別的字段上,是工具類(lèi)型的邏輯封裝。
下面是一個(gè)驗(yàn)證器的例子,它只允許偶數(shù)通過(guò)驗(yàn)證:
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_even(value):
if value % 2 != 0:
raise ValidationError(
_('%(value)s is not an even number'),
params={'value': value},
)
通過(guò)下面的方式,將偶數(shù)驗(yàn)證器應(yīng)用在字段上:
from django.db import models class MyModel(models.Model): even_field = models.IntegerField(validators=[validate_even])
因?yàn)轵?yàn)證器運(yùn)行之前,(輸入的)數(shù)據(jù)會(huì)被轉(zhuǎn)換為 Python 對(duì)象,因此我們可以將同樣的驗(yàn)證器用在 Django form 表單中(事實(shí)上Django為表單提供了另外一些驗(yàn)證器):
from django import forms class MyForm(forms.Form): even_field = forms.IntegerField(validators=[validate_even])
你還可以通過(guò)Python的魔法方法__cal__()編寫(xiě)更復(fù)雜的可配置的驗(yàn)證器,比如Django內(nèi)置的RegexValidator驗(yàn)證器就是這么干的。
驗(yàn)證器也可以是一個(gè)類(lèi),但這時(shí)候就比較復(fù)雜了,需要確保它可以被遷移框架序列化,確保編寫(xiě)了deconstruction()和__eq__()方法。這種做法很難找到參考文獻(xiàn)和博文,要靠自己摸索或者研究DJango源碼。
二、工作機(jī)制
讓我們來(lái)測(cè)試一下上面寫(xiě)的驗(yàn)證器:
>>> from .models import MyModel >>> a = MyModel.objects.create(even_field=3) >>> a <MyModel: MyModel object (1)> >>> a.even_field 3
什么?!??!不是說(shuō)只有偶數(shù)才能通過(guò)驗(yàn)證嗎?這里我提供了數(shù)字3,可是為什么創(chuàng)建成功了??
我們接著在admin站點(diǎn)中注冊(cè)MyModel模型,然后在圖形化界面后臺(tái)中創(chuàng)建MyModel的實(shí)例,你會(huì)發(fā)現(xiàn)這個(gè)時(shí)候驗(yàn)證器起作用了,奇數(shù)是無(wú)法通過(guò)表單驗(yàn)證的!

為什么會(huì)這樣??
這就要從Django的源碼說(shuō)起!
Django是這么設(shè)計(jì)的:
- 模型的驗(yàn)證器不會(huì)在調(diào)用save()方法的時(shí)候自動(dòng)執(zhí)行
- 表單的驗(yàn)證器會(huì)在調(diào)用save()方法的時(shí)候自動(dòng)執(zhí)行
為什么這么設(shè)計(jì)?個(gè)人猜測(cè),Django官方為了序列化、鏈?zhǔn)秸{(diào)用等功能的兼容性,沒(méi)有自動(dòng)進(jìn)行驗(yàn)證操作。
這個(gè)設(shè)計(jì)在源碼中是怎么體現(xiàn)的?
- Django的模型相關(guān)源碼中,沒(méi)有is_valid()方法,也不會(huì)自動(dòng)調(diào)用full_clean() 方法,所以Django不會(huì)自動(dòng)進(jìn)行模型驗(yàn)證。但是它依然提供了四個(gè)重要的驗(yàn)證方法,也就是full_clean() 、clean_fields() 、clean() 和validate_unique(),一會(huì)細(xì)說(shuō)
- Django的表單系統(tǒng)forms的相關(guān)源碼中,表單在save之前會(huì)自動(dòng)執(zhí)行一個(gè)is_valid()方法,這個(gè)方法里會(huì)調(diào)用驗(yàn)證器。
表單的內(nèi)容在其它章節(jié)中講解。
下面介紹Django模型的驗(yàn)證步驟和四個(gè)方法:
模型驗(yàn)證的步驟:
- 如果你手動(dòng)調(diào)用了full_clean()方法,那么會(huì)依次自動(dòng)調(diào)用下面的三個(gè)方法
- clean_fields():驗(yàn)證各個(gè)字段的合法性
- clean():驗(yàn)證模型級(jí)別的合法性
- validate_unique():驗(yàn)證字段的獨(dú)一無(wú)二性
本質(zhì)上,后面三個(gè)方法是具體實(shí)現(xiàn),full_clean()是領(lǐng)頭羊,實(shí)際操作中,你完全可以具體使用其中一個(gè)或多個(gè)。用了full_clean()就等于后面三個(gè)都用。
full_clean()
簽名:Model.full_clean(exclude=None, validate_unique=True)
- exclude用于指定某些字段不進(jìn)行驗(yàn)證,也就是所謂的例外字段
- validate_unique用于指定是否調(diào)用validate_unique()方法
讓我們看下它的源代碼:
def full_clean(self, exclude=None, validate_unique=True):
errors = {}
if exclude is None:
exclude = []
else:
exclude = list(exclude)
try:
self.clean_fields(exclude=exclude) #1
except ValidationError as e:
errors = e.update_error_dict(errors)
try:
self.clean() #2
except ValidationError as e:
errors = e.update_error_dict(errors)
if validate_unique:
for name in errors:
if name != NON_FIELD_ERRORS and name not in exclude:
exclude.append(name)
try:
self.validate_unique(exclude=exclude) #3
except ValidationError as e:
errors = e.update_error_dict(errors)
if errors:
raise ValidationError(errors)
可以看出,它依次調(diào)用了其它三個(gè)方法,如果最后的errors中有內(nèi)容,則拋出ValidationError異常。
我們最好不要去修改full_clean()方法的源代碼,一般也不用重寫(xiě)它,直接調(diào)用即可。
模型的save()方法不會(huì)自動(dòng)調(diào)用full_clean()方法,你必須手動(dòng)調(diào)用。
如果調(diào)用驗(yàn)證器后,拋出ValidationError異常,Django會(huì)將所有的異常信息放置在e.message_dict字典中供使用。比如下面的例子:
# 在視圖中我們可以這么做 from django.core.exceptions import ValidationError try: article.full_clean() except ValidationError as e: # 在這里做一些異常處理操作 pass
在模型定義中我們可以如下重寫(xiě)save()方法,實(shí)現(xiàn)自動(dòng)驗(yàn)證功能,不需要在視圖中反復(fù)調(diào)用了:
# models.py
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_even(value):
if value % 2 != 0:
raise ValidationError(
_('%(value)s is not an even number'),
params={'value': value},
)
from django.db import models
class MyModel(models.Model):
even_field = models.IntegerField(validators=[validate_even])
def save(self, *args, **kwargs): # 重寫(xiě)save方法是關(guān)鍵
try:
self.full_clean()
super().save(*args, **kwargs)
except ValidationError as e:
print('模型驗(yàn)證沒(méi)通過(guò): %s' % e.message_dict)
執(zhí)行過(guò)程展示:
>>> from .models import MyModel
>>> a = MyModel.objects.create(even_field=5)
模型驗(yàn)證沒(méi)通過(guò): {'even_field': ['5 is not an even number']}
這樣,我們就實(shí)現(xiàn)了自動(dòng)的模型驗(yàn)證。
小技巧:可以通過(guò)打印e來(lái)查看,Django怎么封裝的錯(cuò)誤信息,給我們提供了哪些鍵值,比如上例中,我們可以使用e.message_dict['even_field']。
clean_fields()
簽名:Model.clean_fields(exclude=None)
參數(shù)同上,看下它的源代碼:
def clean_fields(self, exclude=None):
if exclude is None:
exclude = []
errors = {}
for f in self._meta.fields:
if f.name in exclude:
continue
raw_value = getattr(self, f.attname)
if f.blank and raw_value in f.empty_values:
continue
try:
setattr(self, f.attname, f.clean(raw_value, self)) #核心是這一句
except ValidationError as e:
errors[f.name] = e.error_list
if errors:
raise ValidationError(errors)
我們最好也不要去修改和重寫(xiě)它的源代碼。
這個(gè)方法本質(zhì)上就是循環(huán)模型中的所有字段,找出其中定義了驗(yàn)證器的那些,并執(zhí)行它們。
我們前面自定義的偶數(shù)驗(yàn)證器,其實(shí)就是在這里被調(diào)用的。
clean()
這個(gè)方法很特別,我們看看它的源代碼:
def clean(self): """ Hook for doing any extra model-wide validation after clean() has been called on every field by self.clean_fields. Any ValidationError raised by this method will not be associated with a particular field; it will have a special-case association with the field defined by NON_FIELD_ERRORS. """ pass
什么都沒(méi)有!實(shí)際上,這個(gè)方法是給你留了個(gè)鉤子,你需要重寫(xiě)它,然后在里面編寫(xiě)模型級(jí)別的驗(yàn)證,比如修改模型的屬性,以及跨字段相關(guān)的驗(yàn)證邏輯。
下面我們通過(guò)一個(gè)例子來(lái)展示它的用法:
import datetime
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
class Article(models.Model):
content = models.TextField()
status = models.CharField(max_length=32)
pub_date = models.DateField(blank=True, null=True)
def clean(self):
# 不允許草稿文章具有發(fā)布日期字段
if self.status == '草稿' and self.pub_date is not None:
raise ValidationError(_('草稿文章尚未發(fā)布,不應(yīng)該有發(fā)布日期!'))
# 如果已發(fā)布的文章還沒(méi)有設(shè)置發(fā)布日期,則將發(fā)布日期設(shè)置為當(dāng)天
if self.status == '已發(fā)布' and self.pub_date is None:
self.pub_date = datetime.date.today()
# 更多Django技術(shù)文章請(qǐng)?jiān)L問(wèn)https://www.liujiangblog.com
說(shuō)明:
- gettext_lazy在這里無(wú)關(guān)緊要
- 在Article模型中重寫(xiě)了clean方法,它不需要接受其它參數(shù)
- 第一個(gè)if判斷,不允許草稿文章具有發(fā)布日期字段。如果你提供了,對(duì)不起,拋出ValidationError異常
- 第二個(gè)if判斷,如果已發(fā)布的文章還沒(méi)有設(shè)置發(fā)布日期,則將發(fā)布日期設(shè)置為當(dāng)天
- 這是一個(gè)跨字段的,全局性的驗(yàn)證方法,它不像我們一開(kāi)始自定義的驗(yàn)證器那樣,不是作為一個(gè)驗(yàn)證器參數(shù)進(jìn)行提供,而是寫(xiě)在clean方法中了,一定要注意區(qū)別。
clean()方法寫(xiě)好了,我們就可以在Article模型中重寫(xiě)save()方法了:
def save(self, *args, **kwargs):
from django.core.exceptions import NON_FIELD_ERRORS
try:
self.full_clean()
super().save(*args, **kwargs)
except ValidationError as e:
print('驗(yàn)證沒(méi)通過(guò): %s' % e.message_dict[NON_FIELD_ERRORS])
注意:這里我們導(dǎo)入了NON_FIELD_ERRORS,在最后打印了e.message_dict[NON_FIELD_ERRORS],這是為什么呢?
因?yàn)?,clean()中編寫(xiě)的都是模型級(jí)別、跨字段的驗(yàn)證方法,沒(méi)有具體和某個(gè)字段綁定,所以Django提供了一個(gè)NON_FIELD_ERRORS關(guān)鍵字,用來(lái)說(shuō)明這不是某個(gè)字段引起的異常,而是非字段相關(guān)的錯(cuò)誤。
如果你非要將錯(cuò)誤定位到某個(gè)具體的字段,也不是不可以的,如下例子所示:
class Article(models.Model):
...
def clean(self):
if self.status == '草稿' and self.pub_date is not None:
raise ValidationError({'pub_date': _('草稿文章尚未發(fā)布,不應(yīng)該有發(fā)布日期!')})
...
甚至,你可以如下方式,映射字段和錯(cuò)誤信息:
raise ValidationError({
'title': ValidationError(_('Missing title.'), code='required'),
'pub_date': ValidationError(_('Invalid date.'), code='invalid'),
})
這些技巧,本質(zhì)上就是給ValidationError異常類(lèi)提供信息參數(shù)。
validate_unique()
簽名:Model.validate_unique(exclude=None)
它的源代碼也很簡(jiǎn)單:
def validate_unique(self, exclude=None): unique_checks, date_checks = self._get_unique_checks(exclude=exclude) errors = self._perform_unique_checks(unique_checks) date_errors = self._perform_date_checks(date_checks) for k, v in date_errors.items(): errors.setdefault(k, []).extend(v) if errors: raise ValidationError(errors)
這個(gè)方法類(lèi)似clean_fields(),只不過(guò)它只用來(lái)驗(yàn)證模型中的唯一性約束是否滿(mǎn)足,而不是字段的值是否滿(mǎn)足驗(yàn)證需求。
如果你提供了exclude參數(shù),那么該參數(shù)包含的所有字段都不會(huì)進(jìn)行唯一性驗(yàn)證。
我們最好也不要去修改和重寫(xiě)它的源代碼。
總結(jié)
Django中模型驗(yàn)證器的使用套路:
- 編寫(xiě)字段級(jí)別的驗(yàn)證器,在字段中作為參數(shù)指定
- 或者編寫(xiě)clean()方法,實(shí)現(xiàn)模型級(jí)別、跨字段的驗(yàn)證功能
- 重寫(xiě)save()方法,調(diào)用full_clean(),實(shí)現(xiàn)全自動(dòng)的驗(yàn)證
- 或者在視圖中,通過(guò)模型實(shí)例調(diào)用full_clean()方法,實(shí)現(xiàn)手動(dòng)驗(yàn)證
三、內(nèi)置驗(yàn)證器
驗(yàn)證器的作用很重要,需求也很廣泛,Django為此內(nèi)置了一些驗(yàn)證器,我們直接拿來(lái)使用即可:
RegexValidator
這是正則匹配驗(yàn)證器。用于對(duì)輸入的值進(jìn)行正則搜索,如果命中,則平安無(wú)事,如果沒(méi)命中則彈出 ValidationError 異常。
數(shù)字簽名:class RegexValidator(regex=None, message=None, code=None, inverse_match=None, flags=0)
參數(shù)說(shuō)明:
- regex:用于匹配的正則表達(dá)式
- message:自定義異常錯(cuò)誤信息。默認(rèn)是"Enter a valid value"
- code:自定義錯(cuò)誤碼。默認(rèn)是"invalid"
- inverse_match:將通過(guò)和不通過(guò)驗(yàn)證的判斷邏輯反轉(zhuǎn)。也就是未命中則平安,命中則出錯(cuò)。
- flags:編譯正則表達(dá)式時(shí)使用的正則flags。默認(rèn)為0。
EmailValidator
數(shù)字簽名:class EmailValidator(message=None, code=None, whitelist=None)
郵件格式驗(yàn)證器。
參數(shù)說(shuō)明:
- message: 自定義錯(cuò)誤信息,默認(rèn)為 "Enter a valid email address"。
- code: 自定義錯(cuò)誤碼,默認(rèn)為"invalid"。
- whitelist:郵件域名白名單,默認(rèn)為['localhost']。
URLValidator
數(shù)字簽名:class URLValidator(schemes=None, regex=None, message=None, code=None)
RegexValidator的子類(lèi),用于驗(yàn)證url的格式是否正確。
schemes:指定URL/URI的協(xié)議模式,默認(rèn)值為['http', 'https', 'ftp', 'ftps']
validate_email
EmailValidator的一個(gè)實(shí)例,未做任何自定義。
validate_slug
一個(gè)確保輸入值是字母、數(shù)字、下劃線(xiàn)和連字符組合的RegexValidator的實(shí)例。
validate_unicode_slug
上面的Unicode編碼版本
validate_ipv4_address
一個(gè)RegexValidator的實(shí)例,用于判斷輸入值是否為ipv4格式
validate_ipv6_address
上面的ipv6版本
validate_ipv46_address
同時(shí)支持ipv4和ipv6
validate_comma_separated_integer_list
判斷輸入是否是一個(gè)以逗號(hào)分隔的數(shù)字列表,一個(gè)RegexValidator的實(shí)例。
int_list_validator
數(shù)字簽名:int_list_validator(sep=', ', message=None, code='invalid', allow_negative=False)
判斷一個(gè)由數(shù)字組成的字符串是否以指定的sep分隔。allow_negative用于反轉(zhuǎn)判斷邏輯。
MaxValueValidator
簽名:class MaxValueValidator(limit_value, message=None)
是否超過(guò)指定最大值
MinValueValidator
簽名:class MinValueValidator(limit_value, message=None)
是否小于指定的最小值
MaxLengthValidator
簽名:class MaxLengthValidator(limit_value, message=None)
輸入值的長(zhǎng)度是否超過(guò)限定值
MinLengthValidator
輸入值的長(zhǎng)度是否小于限定值
DecimalValidator
簽名:lass DecimalValidator(max_digits, decimal_places)
數(shù)字驗(yàn)證器。當(dāng)發(fā)生下面情況時(shí)彈出異常:
- 輸入值超過(guò)max_digits
- 輸入值的位數(shù)超過(guò)decimal_places
- 輸入值大于最大位數(shù)與小數(shù)位數(shù)之差。(待確認(rèn))
FileExtensionValidator
簽名:class FileExtensionValidator(allowed_extensions, message, code)
文件擴(kuò)展名不在合法性列表中。合法性列表通過(guò)參數(shù)allowed_extensions指定。
validate_image_file_extension
通過(guò)pillow庫(kù)確定一個(gè)圖片文件的擴(kuò)展名是合法的
ProhibitNullCharactersValidator
簽名:class ProhibitNullCharactersValidator(message=None, code=None)
對(duì)輸入值進(jìn)行 str(value) 操作,轉(zhuǎn)換成字符串,然后如果這個(gè)字符串中包含1個(gè)以上的空字符'\x00',則驗(yàn)證失敗。
更多特性請(qǐng)參考官方文檔
更多技術(shù)文章請(qǐng)?jiān)L問(wèn): https://www.liujiangblog.com
更多視頻教程請(qǐng)?jiān)L問(wèn): https://www.liujiangblog.com/video/
總結(jié)
到此這篇關(guān)于Django模型驗(yàn)證器介紹與源碼分析的文章就介紹到這了,更多相關(guān)Django模型驗(yàn)證器與源碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Django項(xiàng)目后臺(tái)不掛斷運(yùn)行的方法
今天小編就為大家分享一篇Django項(xiàng)目后臺(tái)不掛斷運(yùn)行的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-08-08
pandas基礎(chǔ)?Series與Dataframe與numpy對(duì)二進(jìn)制文件輸入輸出
這篇文章主要介紹了pandas基礎(chǔ)Series與Dataframe與numpy對(duì)二進(jìn)制文件輸入輸出,series是一種一維的數(shù)組型對(duì)象,它包含了一個(gè)值序列和一個(gè)數(shù)據(jù)標(biāo)簽2022-07-07
Java多線(xiàn)程編程中ThreadLocal類(lèi)的用法及深入
這篇文章主要介紹了Java多線(xiàn)程編程中ThreadLocal類(lèi)的用法及深入,嘗試了自己實(shí)現(xiàn)一個(gè)ThreadLocal類(lèi)以及對(duì)相關(guān)的線(xiàn)程安全問(wèn)題進(jìn)行討論,需要的朋友可以參考下2016-06-06
Django Admin 管理工具的實(shí)現(xiàn)
這篇文章主要介紹了Django Admin 管理工具的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05
Python?代碼智能感知類(lèi)型標(biāo)注與特殊注釋詳解
這篇文章主要為大家介紹了Python?代碼智能感知類(lèi)型標(biāo)注與特殊注釋詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09
pycharm遠(yuǎn)程連接docker容器的操作流程
這篇文章主要給大家介紹了pycharm遠(yuǎn)程連接docker容器的操作流程,文中通過(guò)代碼示例和圖文講解介紹的非常詳細(xì),具有一定的參考價(jià)值,需要的朋友可以參考下2023-08-08
Pyramid將models.py文件的內(nèi)容分布到多個(gè)文件的方法
默認(rèn)的Pyramid代碼結(jié)構(gòu)中,就只有一個(gè)models.py文件,在實(shí)際項(xiàng)目中,如果需要對(duì)models進(jìn)行分類(lèi),放到不同文件下,應(yīng)該怎么辦2013-11-11

