1. 程式人生 > >Django rest framework 序列化元件

Django rest framework 序列化元件

最近在DRF的序列化上踩過了不少坑,特此結合官方文件記錄下,方便日後查閱。

【01】前言

   serializers是什麼?官網是這樣的”Serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types. “翻譯出來就是,將複雜的資料結構,例如ORM中的QuerySet或者Model例項物件轉換成Python內建的資料型別,從而進一步方便資料和json,xml等格式的資料進行互動。

   根據實際的工作經驗,我來總結下serializers的作用: 

  1.將queryset與model例項等進行序列化,轉化成json格式,返回給使用者(api介面)。
  2.將post與patch/put的上來的資料進行驗證。
  3.對post與patch/put資料進行處理。(後面的內容,將用patch表示put/patch更新,博主認為patch更貼近更新的說法)
 

簡單來說,針對get來說,serializers的作用體現在第一條,但如果是其他請求,serializers能夠發揮2,3條的作用!

 

serializers.fieild:我們知道在django中,form也有許多field,那serializers其實也是drf中發揮著這樣的功能。我們先簡單瞭解常用的幾個field。

1. 常用的field 

 CharField、BooleanField、IntegerField、DateTimeField這幾個用得比較多,我們把外來鍵的field放到後面去說!

參考如下的例子:

# 舉例子
mobile = serializers.CharField(max_length=11, min_length=11)
age = serializers.IntegerField(min_value=1, max_value=100)
# format可以設定時間的格式,下面例子會輸出如:2018-3-20 12:10
pay_time = serializers.DateTimeField(read_only=True,format='
%Y-%m-%d %H:%M') is_hot = serializers.BooleanField() # 例如設定商品是否熱銷

2. Core arguments引數

read_only:True表示不允許使用者自己上傳,只能用於api的輸出。如果某個欄位設定了read_only=True,那麼就不需要進行資料驗證,只會在返回時,將這個欄位序列化後返回


  舉個簡單的例子:在使用者進行購物的時候,使用者post訂單時,肯定會產生一個訂單號,而這個訂單號應該由後臺邏輯完成,而不應該由使用者post過來,如果不設定read_only=True,那麼驗證的時候就會報錯。再例如,我們在網上購物時,支付時通常會產生支付狀態,交易號,訂單號,支付時間等欄位,這些欄位都應該設定為read_only=True,即這些欄位都應該由後臺產生然後返回給客戶端;舉例如下:

pay_status = serializers.CharField(read_only=True)
trade_no = serializers.CharField(read_only=True)
order_sn = serializers.CharField(read_only=True)
pay_time = serializers.DateTimeField(read_only=True)

 

在使用者提交訂單的時候,我們在這裡給使用者新增一個欄位,就是支付寶支付的URL
要設定為read_only=True,這樣的話,就不能讓使用者端提交了,而是伺服器端生成
返回給使用者的

alipay_url = serializers.SerializerMethodField(read_only=True)


write_only:與read_only對應;就是使用者post過來的資料,後臺伺服器處理後不會再經過序列化後返回給客戶端;最常見的就是我們在使用手機註冊的驗證碼和填寫的密碼。

required: 顧名思義,就是這個欄位是否必填,例如要求:使用者名稱,密碼等是必須填寫的;不填寫就直接報錯
allow_null/allow_blank:是否允許為NULL/空 。
error_messages:出錯時,資訊提示。
例如我們在定義使用者註冊序列化類時:

code = serializers.CharField(required=True, write_only=True, label="驗證碼", max_length=6, min_length=6,
error_messages={
"blank": "請輸入驗證碼",
"required": "請輸入驗證碼", #當用戶不輸入時提示的錯誤資訊
"max_length": "驗證碼格式錯誤",
"min_length": "驗證碼格式錯誤"
},
help_text="驗證碼")

"""

驗證使用者名稱是否存在,allow_blank 不能為空;表示不允許使用者名稱為空
"""

username = serializers.CharField(required=True, allow_blank=False, label="使用者名稱",
validators=[UniqueValidator(queryset=User.objects.all(), message="使用者已經存在")])
# 同理我們也應該將password設定write_only引數,不讓它返回回去,這樣容易被截獲
password = serializers.CharField(
style={'input_type': 'password'}, help_text="密碼", label="密碼", write_only=True)

label: 欄位顯示設定,如 label=’驗證碼’

help_text: 在指定欄位增加一些提示文字,這兩個欄位作用於api頁面比較有用
style: 說明欄位的型別,這樣看可能比較抽象,看下面例子:

# 在api頁面,輸入密碼就會以*顯示
password = serializers.CharField(
style={'input_type': 'password'})
# 會顯示選項框
color_channel = serializers.ChoiceField(
choices=['red', 'green', 'blue'],
style={'base_template': 'radio.html'})

3. HiddenField

HiddenField的值不依靠輸入,而需要設定預設的值,不需要使用者自己post資料過來,也不會顯式返回給使用者,最常用的就是user!!
  我們在登入情況下,進行一些操作,假設一個使用者去收藏了某一門課,那麼後臺應該自動識別這個使用者,然後使用者只需要將課程的id post過來,那麼這樣的功能,我們配合CurrentUserDefault()實現。

# 這樣就可以直接獲取到當前使用者
user = serializers.HiddenField(
default=serializers.CurrentUserDefault())
再例如,我們在收藏某件商品的時候,後臺要獲取到當前的使用者;相當於前臺只需要傳遞過來一個商品的ID即可;那麼在後臺我根據當前的登入使用者和當前的商品ID即可判斷使用者是否收藏過該商品;這就是一個聯合唯一主鍵的判斷;這同樣需要使用HiddenField。

save instance
  這個標題是官方文件的一個小標題,我覺得用的很好,一眼看出,這是為post和patch所設定的,沒錯,這一部分功能是專門為這兩種請求所設計的,如果只是簡單的get請求,那麼在設定了前面的field可能就能夠滿足這個需求。
  我們在view以及mixins的部落格中提及到,post請求對應create方法,而patch請求對應update方法,這裡提到的create方法與update方法,是指mixins中特定類中的方法。我們看一下原始碼,原始碼具體分析可以參考我的另外一篇部落格:


# 只擷取一部分

class CreateModelMixin(object):

    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()


class UpdateModelMixin(object):

    def update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instance = self.get_object()
        serializer = self.get_serializer(instance, data=request.data, partial=partial)
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)

        if getattr(instance, '_prefetched_objects_cache', None):
        # If 'prefetch_related' has been applied to a queryset, we need to
        # forcibly invalidate the prefetch cache on the instance.
        instance._prefetched_objects_cache = {}

        return Response(serializer.data)

    def perform_update(self, serializer):
        serializer.save()

可以看出,無論是create與update都寫了一行:serializer.save( ),那麼,這一行,到底做了什麼事情,分析一下原始碼。

在serializer.py檔案中:

    def save(self, **kwargs):
        """
        Save and return a list of object instances.
        """
        # Guard against incorrect use of `serializer.save(commit=False)`
        assert 'commit' not in kwargs, (
            "'commit' is not a valid keyword argument to the 'save()' method. "
            "If you need to access data before committing to the database then "
            "inspect 'serializer.validated_data' instead. "
            "You can also pass additional keyword arguments to 'save()' if you "
            "need to set extra attributes on the saved model instance. "
            "For example: 'serializer.save(owner=request.user)'.'"
        )

        validated_data = [
            dict(list(attrs.items()) + list(kwargs.items()))
            for attrs in self.validated_data
        ]

        if self.instance is not None:
            self.instance = self.update(self.instance, validated_data)
            assert self.instance is not None, (
                '`update()` did not return an object instance.'
            )
        else:
            self.instance = self.create(validated_data)
            assert self.instance is not None, (
                '`create()` did not return an object instance.'
            )

        return self.instance

顯然,serializer.save的操作,它去呼叫了serializer的create或update方法,不是mixins中的!!!我們看一下流程圖(以post為例):

 

講了那麼多,我們到底需要幹什麼!過載這兩個方法!!

  如果你的viewset含有post,那麼你需要過載create方法,如果含有patch,那麼就需要過載update方法。


# 假設現在是個部落格,有一個建立文章,與修改文章的功能, model為Article。

class ArticleSerializer(serializers.Serializer):
    user = serializers.HiddenField(
    default=serializers.CurrentUserDefault())
    name = serializers.CharField(max_length=20)
    content = serializers.CharField()

    def create(self, validated_data):

        # 除了使用者,其他資料可以從validated_data這個字典中獲取
        # 注意,users在這裡是放在上下文中的request,而不是直接的request
        user = self.context['request'].user
        name = validated_data['name ']
        content = validated_data['content ']
        return Article.objects.create(**validated_data)

    def update(self, instance, validated_data):

        # 更新的特別之處在於你已經獲取到了這個物件instance
        instance.name = validated_data.get('name')
        instance.content = validated_data.get('content')
        instance.save()
        return instance

可能會有人好奇,系統是怎麼知道,我們需要呼叫serializer的create方法,還是update方法,我們從save( )方法可以看出,判斷的依據是:

if self.instance is not None:pass
那麼我們的mixins的create與update也已經在為開發者設定好了:

# CreateModelMixin
serializer = self.get_serializer(data=request.data)
# UpdateModelMixin
serializer = self.get_serializer(instance, data=request.data, partial=partial)

也就是說,在update通過get_object( )的方法獲取到了instance,然後傳遞給serializer,serializer再根據是否有傳遞instance來判斷來呼叫哪個方法!

Validation自定義驗證邏輯

單獨的validate (這個就類似於Django中Form中的區域性鉤子函式)
  我們在上面提到field,它能起到一定的驗證作用,但很明顯,它存在很大的侷限性,舉個簡單的例子,我們要判斷我們手機號碼,如果使用CharField(max_length=11, min_length=11),它只能確保我們輸入的是11個字元,那麼我們需要自定義!就拿筆者在實際生產環境下的例子來說,光針對使用者輸入的手機號碼,我們在後端就需要進行驗證,例如該手機號碼是否註冊,手機號碼是否合法(例如滿足手機號碼的zhengze表示式,以及要驗證該手機號碼向後臺傳送請求驗證簡訊的頻率等等)

複製程式碼
class SmsSerializer(serializers.Serializer):
"""
為什不用ModelSerializer來完成手機號碼的驗證呢?
因為VerifyCode中還有一個欄位是手機驗證碼欄位,直接使用ModelSerializer來驗證會報錯
因為ModelSerializer會自動生成VerifyCode模型中的所有欄位;但是
我們傳遞過來的就是一個手機號,判斷它是否是合法的
因此使用Serializer來自定義合法性校驗規則,相當於就是鉤子函式
單獨對手機號碼進行驗證
"""

mobile = serializers.CharField(max_length=11)
  # 使用validate_欄位名(self, 欄位名):要麼返回欄位,要麼丟擲異常
def validate_mobile(self, mobile):

"""

驗證手機號碼,記住這裡的validate_欄位名一定要是資料模型中的欄位
:param attrs:
:return:
"""
# 手機號碼是否註冊,查詢UserProfile表即可

if User.objects.filter(mobile=mobile).exists():
raise serializers.ValidationError("使用者已經存在")
# 驗證手機號碼是否合法,這部分應該是在前端做的,當然後臺也需要進行驗證

if not re.match(REGEX_MOBILE, mobile):
raise serializers.ValidationError("手機號碼格式不正確")

# 驗證傳送頻率,如果不做,使用者可以一直向後臺傳送,請求驗證碼;
# 會造成很大的壓力,限制一分鐘只能傳送一次 7-8 09:00
one_minute_ago = datetime.now() - timedelta(hours=0, minutes=1, seconds=0)
"""
如果新增時間在一分鐘以內,它肯定是大於你一分鐘之前的時間的
如果這條記錄存在
"""
if VerifyCode.objects.filter(add_time__gt=one_minute_ago, mobile=mobile):
raise serializers.ValidationError("抱歉,一分鐘只能傳送一次")
# 如果驗證通過,我就將這個mobile返回去,這裡一定要有一個返回
return mobile

聯合validate 這個就類似於全域性鉤子函式

  上面驗證方式,只能驗證一個欄位,如果是兩個欄位聯合在一起進行驗證,那麼我們就可以過載validate( )方法。 

start = serializers.DateTimeField()
finish = serializers.DateTimeField()

def validate(self, attrs):
# 傳進來什麼引數,就返回什麼引數,一般情況下用attrs
if data['start'] > data['finish']:
raise serializers.ValidationError("finish must occur after start")
return attrs

這個方法非常的有用,我們還可以再這裡對一些read_only的欄位進行操作,我們在read_only提及到一個例子,訂單號的生成,我們可以在這步生成一個訂單號,然後新增到attrs這個字典中。看如下的程式碼:

class OrderSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)

pay_status = serializers.CharField(read_only=True)
trade_no = serializers.CharField(read_only=True)
order_sn = serializers.CharField(read_only=True)
pay_time = serializers.DateTimeField(read_only=True)

"""

在使用者提交訂單的時候,我們在這裡給使用者新增一個欄位,就是支付寶支付的URL
要設定為read_only=True,這樣的話,就不能讓使用者端提交了,而是伺服器端生成
返回給使用者的
"""

alipay_url = serializers.SerializerMethodField(read_only=True)

def get_alipay_url(self, obj): # obj就是OrderSerializer物件
alipay = AliPay(
appid="2016091200490227", # 沙箱環境中可以找到
app_notify_url="http://47.92.87.172:8000/alipay/return/",
app_private_key_path=private_key_path, # 個人私鑰
alipay_public_key_path=ali_pub_key_path, # 支付寶的公鑰,驗證支付寶回傳訊息使用,不是你自己的公鑰,
debug=True, # 預設False,上線的時候修改為False即可
return_url="http://47.92.87.172:8000/alipay/return/"
)

url = alipay.direct_pay(
# 一個訂單裡面可能有多個商品,因此subject
# 不適合使用商品名稱
subject=obj.order_sn,
out_trade_no=obj.order_sn,
total_amount=obj.order_mount,
)
re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)

return re_url

def generate_order_sn(self):

"""

後臺系統生成訂單號
這個是系統後臺生成的:
當前時間+userId+隨機數
"""

random_ins = Random()
order_sn = "{time_str}{user_id}{random_num}".format(time_str=time.strftime("%Y%m%d%H%M%S"),
user_id=self.context["request"].user.id,
random_num=random_ins.randint(1000, 9999))
return order_sn

def validate(self, attrs): # 全域性鉤子函式
attrs["order_sn"] = self.generate_order_sn()
return attrs

class Meta:
model = OrderInfo
fields = "__all__"


這個方法運用在modelserializer中,可以剔除掉write_only的欄位,這個欄位只驗證,但不存在於指定的model當中,即不能save( ),可以在這delete掉;例如簡訊驗證碼驗證完畢後就可以刪除了:

def validate(self, attrs):
"""
判斷完畢後刪除驗證碼,因為沒有什麼用了
"""
attrs["mobile"] = attrs["username"]
del attrs["code"]
return attrs

Validators

validators可以直接作用於某個欄位,這個時候,它與單獨的validate作用差不多;當然,drf提供的validators還有很好的功能:UniqueValidator,UniqueTogetherValidator等;UniqueValidator: 指定某一個物件是唯一的,如,使用者名稱只能存在唯一:

username = serializers.CharField(required=True, allow_blank=False, label="使用者名稱", max_length=16, min_length=6,
validators=[UniqueValidator(queryset=User.objects.all(), message="使用者已經存在")],
error_messages={
"blank": "使用者名稱不允許為空",
"required": "請輸入使用者名稱",
"max_length": "使用者名稱長度最長為16位",
"min_length": "使用者名稱長度至少為6位"
})

UniqueTogetherValidator: 聯合唯一,例如我們需要判斷使用者是否收藏了某個商品,前端只需要傳遞過來一個商品ID即可。這個時候就不是像上面那樣單獨作用於某個欄位,而是需要進行聯合唯一的判斷,即使用者ID和商品ID;此時我們需要在Meta中設定。

class UserFavSerializer(serializers.ModelSerializer):
# 獲取到當前使用者
user = serializers.HiddenField(
default=serializers.CurrentUserDefault()
)

class Meta:
model = UserFav
"""
我們需要獲取的是當前登入的user
以後要取消收藏,只需要獲取這裡的id即可
UniqueTogetherValidator作用在多個欄位之上
因為是聯合唯一主鍵
"""
validators = [
UniqueTogetherValidator(
queryset=UserFav.objects.all(),
fields=('user', 'goods'),
message="已經收藏"
)
]
fields = ("user", "goods", "id")

ModelSerializer

  講了很多Serializer的,在這個時候,我還是強烈建議使用ModelSerializer,因為在大多數情況下,我們都是基於model欄位去開發。

好處:
  ModelSerializer已經過載了create與update方法,它能夠滿足將post或patch上來的資料進行進行直接地建立與更新,除非有額外需求,那麼就可以過載create與update方法。
  ModelSerializer在Meta中設定fields欄位,系統會自動進行對映,省去每個欄位再寫一個field。

class UserDetailSerializer(serializers.ModelSerializer):
    """
    使用者詳情序列化
    """

    class Meta:
    model = User
    fields = ("name", "gender", "birthday", "email", "mobile")
    # fields = '__all__': 表示所有欄位
    # exclude = ('add_time',): 除去指定的某些欄位
    # 這三種方式,存在一個即可

ModelSerializer需要解決的2個問題:

  1,某個欄位不屬於指定model,它是write_only,需要使用者傳進來,但我們不能對它進行save( ),因為ModelSerializer是基於Model,這個欄位在Model中沒有對應,這個時候,我們需要過載validate!
如在使用者註冊時,我們需要填寫驗證碼,這個驗證碼只需要驗證,不需要儲存到使用者這個Model中:

def validate(self, attrs):
  del attrs["code"]
  return attrs

  2,某個欄位不屬於指定model,它是read_only,只需要將它序列化傳遞給使用者,但是在這個model中,沒有這個欄位!我們需要用到SerializerMethodField。 

  假設需要返回使用者加入這個網站多久了,不可能維持這樣加入的天數這樣一個數據,一般會記錄使用者加入的時間點,然後當用戶獲取這個資料,我們再計算返回給它。

class UserSerializer(serializers.ModelSerializer): 
    days_since_joined = serializers.SerializerMethodField()
    # 方法寫法:get_ + 欄位
    def get_days_since_joined(self, obj):
    # obj指這個model的物件
        return (now() - obj.date_joined).days
    
    class Meta:
        model = User

當然,這個的SerializerMethodField用法還相對簡單一點,後面還會有比較複雜的情況。

關於外來鍵的serializers
  講了那麼多,終於要研究一下外來鍵啦~
  其實,外來鍵的field也比較簡單,如果我們直接使用serializers.Serializer,那麼直接用PrimaryKeyRelatedField就解決了。
  假設現在有一門課python入門教學(course),它的類別是python(catogory)。

 

# 指定queryset
category = serializers.PrimaryKeyRelatedField(queryset=CourseCategory.objects.all(), required=True)
ModelSerializer就更簡單了,直接通過對映就好了
不過這樣只是使用者獲得的只是一個外來鍵類別的id,並不能獲取到詳細的資訊,如果想要獲取到具體資訊,那需要巢狀serializer:

category = CourseCategorySerializer()
注意:上面兩種方式,外來鍵都是正向取得,下面介紹怎麼反向去取,如,我們需要獲取python這個類別下,有什麼課程。
  首先,在課程course的model中,需要在外來鍵中設定related_name:

class Course(model.Model):
category = models.ForeignKey(CourseCategory, related_name='courses')

# 反向取課程,通過related_name
# 一對多,一個類別下有多個課程,一定要設定many=True
courses = CourseSerializer(many=True)
寫到這裡,我們的外來鍵就基本講完了!還有一個小問題:我們在上面提到ModelSerializer需要解決的第二個問題中,其實還有一種情況,就是某個欄位屬於指定model,但不能獲取到相關資料。
  假設現在是一個多級分類的課程,例如,程式語言–>python–>python入門學習課程,程式語言與python屬於類別,另外一個屬於課程,程式語言類別是python類別的一個外來鍵,而且屬於同一個model,實現方法:

parent_category = models.ForeignKey('self', null=True, blank=True,
verbose_name='父類目別',
related_name='sub_cat')
現在獲取程式語言下的課程,顯然無法直接獲取到python入門學習這個課程,因為它們兩沒有外來鍵關係。SerializerMethodField( )也可以解決這個問題,只要在自定義的方法中實現相關的邏輯即可!

courses = SerializerMethodField()
def get_courses(self, obj):
all_courses = Course.objects.filter(category__parent_category_id=obj.id)
courses_serializer = CourseSerializer(all_course, many=True, 
context={'request': self.context['request']})
return courses_serializer.data

上面的例子看起來有點奇怪,因為我們在SerializerMethodField()嵌套了serializer,就需要自己進行序列化,然後再從data就可以取出json資料。 

  可以看到傳遞的引數是分別是:queryset,many=True多個物件,context上下文。這個context十分關鍵,如果不將request傳遞給它,在序列化的時候,圖片與檔案這些Field不會再前面加上域名,也就是說,只會有/media/img…這樣的路徑!

  以上就是關於DRF的Serializer的小結,如果有錯漏,煩請指正,後面我將把自己工作中遇到的坑分享出來,希望對大家有幫助!