轉發請註明來源
在Django的模型欄位引數中,有一個引數叫做validators
,這個引數是用來指定當前欄位需要使用的驗證器,也就是對欄位資料的合法性進行驗證,比如大小、型別等。
Django的驗證器可以分為模型相關的驗證器和表單相關的驗證器,它們基本類似,但在使用上有區別。
本文討論的是模型相關的驗證器。
一、自定義驗證器
一個驗證器其實就是一個可呼叫的物件(函式或類),接收一個初始輸入值作為引數,對這個值進行一系列邏輯判斷,如果不滿足某些規則或者條件,則表示驗證不通過,丟擲一個ValidationError
異常。如果滿足條件則通過驗證,不返回任何內容(也就是預設的return None),可以繼續下一步。
驗證器具有重要作用,可以被重用在別的欄位上,是工具型別的邏輯封裝。
下面是一個驗證器的例子,它只允許偶數通過驗證:
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])
因為驗證器執行之前,(輸入的)資料會被轉換為 Python 物件,因此我們可以將同樣的驗證器用在 Django form 表單中(事實上Django為表單提供了另外一些驗證器):
from django import forms
class MyForm(forms.Form):
even_field = forms.IntegerField(validators=[validate_even])
你還可以通過Python的魔法方法__cal__()
編寫更復雜的可配置的驗證器,比如Django內建的RegexValidator
驗證器就是這麼幹的。
驗證器也可以是一個類,但這時候就比較複雜了,需要確保它可以被遷移框架序列化,確保編寫了deconstruction()
和__eq__()
方法。這種做法很難找到參考文獻和博文,要靠自己摸索或者研究DJango原始碼。
二、工作機制
讓我們來測試一下上面寫的驗證器:
>>> from .models import MyModel
>>> a = MyModel.objects.create(even_field=3)
>>> a
<MyModel: MyModel object (1)>
>>> a.even_field
3
什麼?!!!不是說只有偶數才能通過驗證嗎?這裡我提供了數字3,可是為什麼建立成功了??
我們接著在admin站點中註冊MyModel模型,然後在圖形化介面後臺中建立MyModel的例項,你會發現這個時候驗證器起作用了,奇數是無法通過表單驗證的!
為什麼會這樣??
這就要從Django的原始碼說起!
Django是這麼設計的:
- 模型的驗證器不會在呼叫save()方法的時候自動執行
- 表單的驗證器會在呼叫save()方法的時候自動執行
為什麼這麼設計?個人猜測,Django官方為了序列化、鏈式呼叫等功能的相容性,沒有自動進行驗證操作。
這個設計在原始碼中是怎麼體現的?
- Django的模型相關原始碼中,沒有
is_valid()
方法,也不會自動呼叫full_clean()
方法,所以Django不會自動進行模型驗證。但是它依然提供了四個重要的驗證方法,也就是full_clean()
、clean_fields()
、clean()
和validate_unique()
,一會細說 - Django的表單系統forms的相關原始碼中,表單在save之前會自動執行一個
is_valid()
方法,這個方法裡會呼叫驗證器。
表單的內容在其它章節中講解。
下面介紹Django模型的驗證步驟和四個方法:
模型驗證的步驟:
- 如果你手動呼叫了
full_clean()
方法,那麼會依次自動呼叫下面的三個方法 clean_fields()
:驗證各個欄位的合法性clean()
:驗證模型級別的合法性validate_unique()
:驗證欄位的獨一無二性
本質上,後面三個方法是具體實現,full_clean()
是領頭羊,實際操作中,你完全可以具體使用其中一個或多個。用了full_clean()
就等於後面三個都用。
full_clean()
簽名:Model.full_clean(exclude=None, validate_unique=True)
- exclude用於指定某些欄位不進行驗證,也就是所謂的例外欄位
- validate_unique用於指定是否呼叫
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)
可以看出,它依次呼叫了其它三個方法,如果最後的errors中有內容,則丟擲ValidationError
異常。
我們最好不要去修改full_clean()
方法的原始碼,一般也不用重寫它,直接呼叫即可。
模型的save()方法不會自動呼叫full_clean()
方法,你必須手動呼叫。
如果呼叫驗證器後,丟擲ValidationError
異常,Django會將所有的異常資訊放置在e.message_dict
字典中供使用。比如下面的例子:
# 在檢視中我們可以這麼做
from django.core.exceptions import ValidationError
try:
article.full_clean()
except ValidationError as e:
# 在這裡做一些異常處理操作
pass
在模型定義中我們可以如下重寫save()方法,實現自動驗證功能,不需要在檢視中反覆呼叫了:
# 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): # 重寫save方法是關鍵
try:
self.full_clean()
super().save(*args, **kwargs)
except ValidationError as e:
print('模型驗證沒通過: %s' % e.message_dict)
執行過程展示:
>>> from .models import MyModel
>>> a = MyModel.objects.create(even_field=5)
模型驗證沒通過: {'even_field': ['5 is not an even number']}
這樣,我們就實現了自動的模型驗證。
小技巧:可以通過列印e來檢視,Django怎麼封裝的錯誤資訊,給我們提供了哪些鍵值,比如上例中,我們可以使用e.message_dict['even_field']
。
clean_fields()
簽名:Model.clean_fields(exclude=None)
引數同上,看下它的原始碼:
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)
我們最好也不要去修改和重寫它的原始碼。
這個方法本質上就是迴圈模型中的所有欄位,找出其中定義了驗證器的那些,並執行它們。
我們前面自定義的偶數驗證器,其實就是在這裡被呼叫的。
clean()
這個方法很特別,我們看看它的原始碼:
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
什麼都沒有!實際上,這個方法是給你留了個鉤子,你需要重寫它,然後在裡面編寫模型級別的驗證,比如修改模型的屬性,以及跨欄位相關的驗證邏輯。
下面我們通過一個例子來展示它的用法:
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):
# 不允許草稿文章具有釋出日期欄位
if self.status == '草稿' and self.pub_date is not None:
raise ValidationError(_('草稿文章尚未釋出,不應該有釋出日期!'))
# 如果已釋出的文章還沒有設定釋出日期,則將釋出日期設定為當天
if self.status == '已釋出' and self.pub_date is None:
self.pub_date = datetime.date.today()
# 更多Django技術文章請訪問https://www.liujiangblog.com
說明:
- gettext_lazy在這裡無關緊要
- 在Article模型中重寫了clean方法,它不需要接受其它引數
- 第一個if判斷,不允許草稿文章具有釋出日期欄位。如果你提供了,對不起,丟擲ValidationError異常
- 第二個if判斷,如果已釋出的文章還沒有設定釋出日期,則將釋出日期設定為當天
- 這是一個跨欄位的,全域性性的驗證方法,它不像我們一開始自定義的驗證器那樣,不是作為一個驗證器引數進行提供,而是寫在clean方法中了,一定要注意區別。
clean()
方法寫好了,我們就可以在Article模型中重寫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('驗證沒通過: %s' % e.message_dict[NON_FIELD_ERRORS])
注意:這裡我們匯入了NON_FIELD_ERRORS
,在最後列印了e.message_dict[NON_FIELD_ERRORS]
,這是為什麼呢?
因為,clean()
中編寫的都是模型級別、跨欄位的驗證方法,沒有具體和某個欄位繫結,所以Django提供了一個NON_FIELD_ERRORS
關鍵字,用來說明這不是某個欄位引起的異常,而是非欄位相關的錯誤。
如果你非要將錯誤定位到某個具體的欄位,也不是不可以的,如下例子所示:
class Article(models.Model):
...
def clean(self):
if self.status == '草稿' and self.pub_date is not None:
raise ValidationError({'pub_date': _('草稿文章尚未釋出,不應該有釋出日期!')})
...
甚至,你可以如下方式,對映欄位和錯誤資訊:
raise ValidationError({
'title': ValidationError(_('Missing title.'), code='required'),
'pub_date': ValidationError(_('Invalid date.'), code='invalid'),
})
這些技巧,本質上就是給ValidationError
異常類提供資訊引數。
validate_unique()
簽名:Model.validate_unique(exclude=None)
它的原始碼也很簡單:
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)
這個方法類似clean_fields()
,只不過它只用來驗證模型中的唯一性約束是否滿足,而不是欄位的值是否滿足驗證需求。
如果你提供了exclude引數,那麼該引數包含的所有欄位都不會進行唯一性驗證。
我們最好也不要去修改和重寫它的原始碼。
總結
Django中模型驗證器的使用套路:
- 編寫欄位級別的驗證器,在欄位中作為引數指定
- 或者編寫clean()方法,實現模型級別、跨欄位的驗證功能
- 重寫save()方法,呼叫
full_clean()
,實現全自動的驗證 - 或者在檢視中,通過模型例項呼叫
full_clean()
方法,實現手動驗證
三、內建驗證器
驗證器的作用很重要,需求也很廣泛,Django為此內建了一些驗證器,我們直接拿來使用即可:
RegexValidator
這是正則匹配驗證器。用於對輸入的值進行正則搜尋,如果命中,則平安無事,如果沒命中則彈出 ValidationError
異常。
數字簽名:class RegexValidator(regex=None, message=None, code=None, inverse_match=None, flags=0)
引數說明:
- regex:用於匹配的正規表示式
- message:自定義異常錯誤資訊。預設是
"Enter a valid value"
- code:自定義錯誤碼。預設是
"invalid"
- inverse_match:將通過和不通過驗證的判斷邏輯反轉。也就是未命中則平安,命中則出錯。
- flags:編譯正規表示式時使用的正則flags。預設為0。
EmailValidator
數字簽名:class EmailValidator(message=None, code=None, whitelist=None)
郵件格式驗證器。
引數說明:
- message: 自定義錯誤資訊,預設為 "Enter a valid email address"。
- code: 自定義錯誤碼,預設為"invalid"。
- whitelist:郵件域名白名單,預設為
['localhost']
。
URLValidator
數字簽名:class URLValidator(schemes=None, regex=None, message=None, code=None)
RegexValidator的子類,用於驗證url的格式是否正確。
schemes:指定URL/URI的協議模式,預設值為['http', 'https', 'ftp', 'ftps']
validate_email
EmailValidator的一個例項,未做任何自定義。
validate_slug
一個確保輸入值是字母、數字、下劃線和連字元組合的RegexValidator的例項。
validate_unicode_slug
上面的Unicode編碼版本
validate_ipv4_address
一個RegexValidator的例項,用於判斷輸入值是否為ipv4格式
validate_ipv6_address
上面的ipv6版本
validate_ipv46_address
同時支援ipv4和ipv6
validate_comma_separated_integer_list
判斷輸入是否是一個以逗號分隔的數字列表,一個RegexValidator的例項。
int_list_validator
數字簽名:int_list_validator(sep=', ', message=None, code='invalid', allow_negative=False)
判斷一個由數字組成的字串是否以指定的sep分隔。allow_negative
用於反轉判斷邏輯。
MaxValueValidator
簽名:class MaxValueValidator(limit_value, message=None)
是否超過指定最大值
MinValueValidator
簽名:class MinValueValidator(limit_value, message=None)
是否小於指定的最小值
MaxLengthValidator
簽名:class MaxLengthValidator(limit_value, message=None)
輸入值的長度是否超過限定值
MinLengthValidator
輸入值的長度是否小於限定值
DecimalValidator
簽名:lass DecimalValidator(max_digits, decimal_places)
數字驗證器。當發生下面情況時彈出異常:
- 輸入值超過max_digits
- 輸入值的位數超過decimal_places
- 輸入值大於最大位數與小數位數之差。(待確認)
FileExtensionValidator
簽名:class FileExtensionValidator(allowed_extensions, message, code)
副檔名不在合法性列表中。合法性列表通過引數allowed_extensions
指定。
validate_image_file_extension
通過pillow庫確定一個圖片檔案的副檔名是合法的
ProhibitNullCharactersValidator
簽名:class ProhibitNullCharactersValidator(message=None, code=None)
對輸入值進行 str(value)
操作,轉換成字串,然後如果這個字串中包含1個以上的空字元'\x00'
,則驗證失敗。