1. 程式人生 > >DRF之解析器組件及序列化組件

DRF之解析器組件及序列化組件

除了 queryset 服務器 字段 color stat 需求 使用方式 更新

  通過上一篇博客的學習,我們已經有了一個共識,Django無法處理application/json協議請求的數據,即,如果用戶通過application/json協議發送請求數據到達Django服務器,我們通過request.POST獲取到的是一個空對象。

  Django RestFramework幫助我們實現了處理application/json協議請求的數據,另外,我們也提到,如果不使用DRF,直接從request.body裏面拿到原始的客戶端請求的字節數據,經過decode,然後json反序列化之後,也可以得到一個Python字典類型的數據。

  但是,這種方式並不被推薦,因為已經有了非常優秀的第三方工具,那就是Django RestFramework的解析器組件,這是我們今天要學習的第一個非常重要的知識點,咱們今天要學習的另一個也是至關重要的知識點叫做序列化組件。

  序列化在上一篇博客中也已經提到了,幫助我們快速的進行符合規範的接口開發,也就是增刪改查接口,包含錯誤信息定義,成功之後的返回信息定義等等。

  接下來,咱們就開始學習DRF的這兩個重要組件。

知識點復習回顧一:三元運算

  三元運算能夠簡化我們的代碼,請看如下代碼:

技術分享圖片
# 定義兩個變量
a = 1
b = 2
# 判斷a的真假值,如果為True,則將判斷表達式的前面的值賦給c,否則將判斷表達式後面的值賦給c
c = a if a else b

print(c) # 1  # 因為a的真假值判斷為True,所以c為1
# 定義兩個變量
a = 0
b = 2
#
判斷a的真假值,如果為True,則將判斷表達式的前面的值賦給c,否則將判斷表達式後面的值賦給c c = a if a else b print(c) # 2 # 因為a的真假值判斷為False,所以c為2
View Code

知識點復習回顧二:列表推導式

  列表推導式的返回結果仍然是一個列表,假設有列表li = [1, 2, 3, 4], 請同學們思考,如果將li列表裏面的每一個元素乘以2得到一個新的列表,請看下面的代碼:

li = [1, 2, 3, 4]
# 方式一:使用普通for循環
new_list = list()
for item in li:
    new_list.append(item 
* 2) # 方式二:使用列表推導式 li = [ x * 2 for x in li]

  可見,列表推導式能夠大大簡化代碼,再看下面的代碼:

class Father(object):
    country = "china"


class Person(Father):
    def __init__(self, name, age):
        self.name = name
        self.age = age


p = Person("pizza", 18)
print(p.__dict__)       # {‘name‘: ‘pizza‘, ‘age‘: 18}
print(Person.__dict__)  # {‘__module__‘: ‘__main__‘, ‘__init__‘: <function Person.__init__ at 0x103f132f0>, ‘__doc__‘: None}
print(p.name)           # pizza
print(p.age)            # 18
print(p.country)        # china 如果對象不存在這個屬性,則會到其父類中查找這個屬性
print(p.hobby)          # 如果在父類中也找不到這個屬性,則會報錯:AttributeError: ‘Person‘ object has no attribute ‘hobby‘

  對象的屬性查找首先會在該對象的一個名為dict的字典中查找這個屬性,如果找不到,則會到其父類中查找這個屬性,如果在父類中都也找不到對應的屬性,這會拋出異常AttributeError,我們可以通過在類中定義一個getattr來重定向未查找到屬性後的行為,請看下面的代碼:

class Father(object):
    country = "china"


class Person(Father):
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __getattr__(self, value):
        raise ValueError("屬性%s不存在" % value)


p = Person("pizza", 18)
print(p.hobby)  # ValueError: 屬性hobby不存在

  可以看到,我們能夠重新定義異常,也可以做其他任何事情,這就是getattr,一句話總結,通過對象查找屬性,如果找不到屬性,且該對象有getattr方法,那麽getattr方法會被執行,至於執行什麽邏輯,我們可以自定義。

知識點復習回顧四:Django settings文件查找順序

  在使用Django的時候,應該是經常會用到它的settings文件,通過在settings文件裏面定義變量,我們可以在程序的任何地方使用這個變量,方便好用,比如,假設我在settings裏面定義了一個變量NAME=”Pizza”, 雖然可以在項目的任何地方使用:

>>> from drf_server import settings
>>> print(settings.NAME) # Pizza

  但是,這種方式並不是被推薦和建議的,因為除了項目本身的settings文件之外,Django程序本身也有許多配置變量,都存儲在django/conf/global_setting.py模塊裏面,包括緩存、數據庫、秘鑰等,如果我們只是from drf_server import settings導入了項目本身的配置信息,當需要用到Django默認的配置信息的時候,還需要再次導入,from django.conf import settings,所以建議的使用方式是:

>>> from django.conf import settings
>>> print(settings.NAME)

  使用上面的方式,我們除了可以使用自定義的配置信息(NAME)外,還可以使用global_settings中的配置信息,不需要重復導入,Django查找變量的順序是先從用戶的settings裏面查找,然後在global_settings中查找,如果用戶的settings中找到了,則不會繼續查找global_settings中的配置信息,假設我在用戶的settings裏面定義了NAME=”Pizza”, 在global_settings中定義了NAME=”Alex”,請看下面的打印結果:

>>> from django.conf import settings
>>> print(settings.NAME) # Pizza

  可見,這種方式更加靈活高效,建議大家使用。

知識點復習回顧五:Django原生serializer

  上一篇博客講到,我們可以自定義符合REST規範的接口,請看下面的代碼:

from django.db import models

# Create your models here.


class Courses(models.Model):
    title = models.CharField(max_length=32)
    description = models.CharField(max_length=128)
class CoursesView(View):
    def get(self, request):
        courses = list()

        for item in Courses.objects.all():
            course = {
                "title": item.title,
                "description": item.description
            }

            courses.append(course)

        return HttpResponse(json.dumps(courses, ensure_ascii=False))

  通過上面的方式,我們定義出了符合規範的返回數據,加上符合規範的url,我們可以說,手動方式進行REST開發也是完全沒有問題的,但是,企業最註重的是開發效率,而不是程序員實現需求的方式,理論上來說,我們可以通過任何方式,但是我們應該盡可能的采用高效的、靈活的、強大的工具來幫助我們完成重復的事情,所以我們需要學習DRF,它提供了很多的功能,在講DRF的序列化之前,我們來了解另一個知識,那就是,Django框架原生的序列化功能,即Django原生serializer,它的使用方式如下:

from django.core.serializers import serialize

class StudentView(APIView):

    def get(self, request):
       
        origin_students = Student.objects.all()
        serialized_students = serialize("json", origin_students)

        return HttpResponse(serialized_students)

  使用方式非常簡單,導入模塊之後,將需要的格式和queryset傳給serialize進行序列化,然後返回序列化後的數據。

  如果你的項目僅僅只是需要序列化一部分數據,不需要用到諸如認證、權限等等其他功能,可以使用Django原生serializer,否則建議使用DRF。

解析器組件

首先,來看看解析器組件的使用,稍後我們一起剖析其源碼:

from django.http import JsonResponse

from rest_framework.views import APIView
from rest_framework.parsers import JSONParser, FormParser
# Create your views here.


class LoginView(APIView):
    parser_classes = [FormParser]

    def get(self, request):
        return render(request, parserver/login.html)

    def post(self, request):
        # request是被drf封裝的新對象,基於django的request
        # request.data是一個property,用於對數據進行校驗
        # request.data最後會找到self.parser_classes中的解析器
        # 來實現對數據進行解析
        
        print(request.data)  # {‘username‘: ‘alex‘, ‘password‘: 123}

        return JsonResponse({"status_code": 200, "code": "OK"})

使用方式非常簡單,分為如下兩步:

  • from rest_framework.views import APIView

  • 繼承APIView

  • 直接使用request.data就可以獲取Json數據

如果你只需要解析Json數據,不允許任何其他類型的數據請求,可以這樣做:

  • from rest_framework.parsers import JsonParser

  • 給視圖類定義一個parser_classes變量,值為列表類型[JsonParser]

  • 如果parser_classes = [], 那就不處理任何數據類型的請求了

  我們需要弄明白,在整個流程中,request對象是什麽時候才出現的,是在綁定url和處理視圖之間的映射關系的時候嗎?我們來看看源碼:

技術分享圖片
@classonlymethod
def as_view(cls, **initkwargs):
    """Main entry point for a request-response process."""
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError("You tried to pass in the %s method name as a "
                            "keyword argument to %s(). Don‘t do that."
                            % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

def view(request, *args, **kwargs):
    self = cls(**initkwargs)
    if hasattr(self, get) and not hasattr(self, head):
        self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs

    # take name and docstring from class
    update_wrapper(view, cls, updated=())

    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view
View Code

  在執行view函數的時候,那麽什麽時候執行view函數呢?當然是請求到來,根據url查找映射表,找到視圖函數,然後執行view函數並傳入request對象,所以,如果是我,我可以在這個視圖函數裏面加入處理application/json的功能:

技術分享圖片
@classonlymethod
def as_view(cls, **initkwargs):
    """Main entry point for a request-response process."""
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError("You tried to pass in the %s method name as a "
                            "keyword argument to %s(). Don‘t do that."
                            % (key, cls.__name__))
            if not hasattr(cls, key):
                raise TypeError("%s() received an invalid keyword %r. as_view "
                                "only accepts arguments that are already "
                                "attributes of the class." % (cls.__name__, key))

def view(request, *args, **kwargs):
    if request.content_type == "application/json":
        import json
        return HttpResponse(json.dumps({"error": "Unsupport content type!"}))

    self = cls(**initkwargs)
    if hasattr(self, get) and not hasattr(self, head):
        self.head = self.get
        self.request = request
        self.args = args
        self.kwargs = kwargs
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs

    # take name and docstring from class
    update_wrapper(view, cls, updated=())

    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view
View Code

解析器組件源碼剖析

請看下圖:

技術分享圖片

  上圖詳細描述了整個過程,最重要的就是重新定義的request對象,和parser_classes變量,也就是我們在上面使用的類變量。好了,通過分析源碼,驗證了我們的猜測。

序列化組件

  序列化組件的使用

  定義幾個 model:

from django.db import models

# Create your models here.


class Publish(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    city = models.CharField(max_length=32)
    email = models.EmailField()

    def __str__(self):
        return self.name


class Author(models.Model):
    nid = models.AutoField(primary_key=True)
    name = models.CharField(max_length=32)
    age = models.IntegerField()

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=32)
    publishDate = models.DateField()
    price = models.DecimalField(max_digits=5, decimal_places=2)
    publish = models.ForeignKey(to="Publish", to_field="nid", on_delete=models.CASCADE)
    authors = models.ManyToManyField(to="Author")

    def __str__(self):
        return self.title

  通過序列化組件進行GET接口設計

  首先,設計url,本次我們只設計GET和POST兩種接口:

from django.urls import re_path

from serializers import views

urlpatterns = [
    re_path(rbooks/$, views.BookView.as_view())
]

  我們新建一個名為app_serializers.py的模塊,將所有的序列化的使用集中在這個模塊裏面,對程序進行解耦:

# -*- coding: utf-8 -*-
from rest_framework import serializers

from .models import Book


class BookSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=128)
    publish_date = serializers.DateTimeField()
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    publish = serializers.CharField(max_length=32)
    authors = serializers.CharField(max_length=32)

接著,使用序列化組件,開始寫視圖類:

# -*- coding: utf-8 -*-
from rest_framework.views import APIView
from rest_framework.response import Response

# 當前app中的模塊
from .models import Book
from .app_serializer import BookSerializer

# Create your views here.

class BookView(APIView):
    def get(self, request):
        origin_books = Book.objects.all()
        serialized_books = BookSerializer(origin_books, many=True)

        return Response(serialized_books.data)

  如此簡單,我們就已經,通過序列化組件定義了一個符合標準的接口,定義好model和url後,使用序列化組件的步驟如下:

  • 導入序列化組件:from rest_framework import serializers

  • 定義序列化類,繼承serializers.Serializer(建議單獨創建一個專用的模塊用來存放所有的序列化類):class BookSerializer(serializers.Serializer):pass

  • 定義需要返回的字段(字段類型可以與model中的類型不一致,參數也可以調整),字段名稱必須與model中的一致

  • 在GET接口邏輯中,獲取QuerySet

  • 開始序列化:將QuerySet作業第一個參數傳給序列化類,many默認為False,如果返回的數據是一個列表嵌套字典的多個對象集合,需要改為many=True

  • 返回:將序列化對象的data屬性返回即可

  上面的接口邏輯中,我們使用了Response對象,它是DRF重新封裝的響應對象。該對象在返回響應數據時會判斷客戶端類型(瀏覽器或POSTMAN),如果是瀏覽器,它會以web頁面的形式返回,如果是POSTMAN這類工具,就直接返回Json類型的數據。

  此外,序列化類中的字段名也可以與model中的不一致,但是需要使用source參數來告訴組件原始的字段名,如下:

class BookSerializer(serializers.Serializer):
    BookTitle = serializers.CharField(max_length=128, source="title")
    publishDate = serializers.DateTimeField()
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    # source也可以用於ForeignKey字段
    publish = serializers.CharField(max_length=32, source="publish.name")
    authors = serializers.CharField(max_length=32)

  下面是通過POSTMAN請求該接口後的返回數據,大家可以看到,除ManyToManyField字段不是我們想要的外,其他的都沒有任何問題:

[
    {
        "title": "Python入門",
        "publishDate": null,
        "price": "119.00",
        "publish": "浙江大學出版社",
        "authors": "serializers.Author.None"
    },
    {
        "title": "Python進階",
        "publishDate": null,
        "price": "128.00",
        "publish": "清華大學出版社",
        "authors": "serializers.Author.None"
    }
]

  那麽,多對多字段如何處理呢?如果將source參數定義為”authors.all”,那麽取出來的結果將是一個QuerySet,對於前端來說,這樣的數據並不是特別友好,我們可以使用如下方式:

class BookSerializer(serializers.Serializer):
    title = serializers.CharField(max_length=32)
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    publishDate = serializers.DateField()
    publish = serializers.CharField()
    publish_name = serializers.CharField(max_length=32, read_only=True, source=publish.name)
    publish_email = serializers.CharField(max_length=32, read_only=True, source=publish.email)
    # authors = serializers.CharField(max_length=32, source=‘authors.all‘)
    authors_list = serializers.SerializerMethodField()

    def get_authors_list(self, authors_obj):
        authors = list()
        for author in authors_obj.authors.all():
            authors.append(author.name)

        return authors

   請註意,get_必須與字段名稱一致,否則會報錯。

  通過序列化組件進行POST接口設計

  接下來,我們設計POST接口,根據接口規範,我們不需要新增url,只需要在視圖類中定義一個POST方法即可,序列化類不需要修改,如下:

# -*- coding: utf-8 -*-
from rest_framework.views import APIView
from rest_framework.response import Response

# 當前app中的模塊
from .models import Book
from .app_serializer import BookSerializer

# Create your views here.


class BookView(APIView):
    def get(self, request):
        origin_books = Book.objects.all()
        serialized_books = BookSerializer(origin_books, many=True)

        return Response(serialized_books.data)

    def post(self, request):
        verified_data = BookSerializer(data=request.data)

        if verified_data.is_valid():
            book = verified_data.save()
            # 可寫字段通過序列化添加成功之後需要手動添加只讀字段
            authors = Author.objects.filter(nid__in=request.data[authors])
            book.authors.add(*authors)

            return Response(verified_data.data)
        else:
            return Response(verified_data.errors)

POST接口的實現方式,如下:

  • url定義:需要為post新增url,因為根據規範,url定位資源,http請求方式定義用戶行為

  • 定義post方法:在視圖類中定義post方法

  • 開始序列化:通過我們上面定義的序列化類,創建一個序列化對象,傳入參數data=request.data(application/json)數據

  • 校驗數據:通過實例對象的is_valid()方法,對請求數據的合法性進行校驗

  • 保存數據:調用save()方法,將數據插入數據庫

  • 插入數據到多對多關系表:如果有多對多字段,手動插入數據到多對多關系表

  • 返回:將插入的對象返回

  請註意,因為多對多關系字段是我們自定義的,而且必須這樣定義,返回的數據才有意義,而用戶插入數據的時候,serializers.Serializer沒有實現create,我們必須手動插入數據,就像這樣:

# 第二步, 創建一個序列化類,字段類型不一定要跟models的字段一致
class BookSerializer(serializers.Serializer):
    # nid = serializers.CharField(max_length=32)
    title = serializers.CharField(max_length=128)
    price = serializers.DecimalField(max_digits=5, decimal_places=2)
    publish = serializers.CharField()
    # 外鍵字段, 顯示__str__方法的返回值
    publish_name = serializers.CharField(max_length=32, read_only=True, source=publish.name)
    publish_city = serializers.CharField(max_length=32, read_only=True, source=publish.city)
    # authors = serializers.CharField(max_length=32) # book_obj.authors.all()

    # 多對多字段需要自己手動獲取數據,SerializerMethodField()
    authors_list = serializers.SerializerMethodField()

    def get_authors_list(self, book_obj):
        author_list = list()

        for author in book_obj.authors.all():
            author_list.append(author.name)

        return author_list

    def create(self, validated_data):
        # {‘title‘: ‘Python666‘, ‘price‘: Decimal(‘66.00‘), ‘publish‘: ‘2‘}
        validated_data[publish_id] = validated_data.pop(publish)
        book = Book.objects.create(**validated_data)

        return book

    def update(self, instance, validated_data):
        # 更新數據會調用該方法
        instance.title = validated_data.get(title, instance.title)
        instance.publishDate = validated_data.get(publishDate, instance.publishDate)
        instance.price = validated_data.get(price, instance.price)
        instance.publish_id = validated_data.get(publish, instance.publish.nid)

        instance.save()

        return instance

  這樣就會非常復雜化程序,如果我希望序列化類自動插入數據呢?

  這是問題一:如何讓序列化類自動插入數據?

  另外問題二:如果字段很多,那麽顯然,寫序列化類也會變成一種負擔,有沒有更加簡單的方式呢?

  答案是肯定的,我們可以這樣做:

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book

        fields = (title,
                  price,
                  publish,
                  authors,
                  author_list,
                  publish_name,
                  publish_city
                  )
        extra_kwargs = {
            publish: {write_only: True},
            authors: {write_only: True}
        }

    publish_name = serializers.CharField(max_length=32, read_only=True, source=publish.name)
    publish_city = serializers.CharField(max_length=32, read_only=True, source=publish.city)

    author_list = serializers.SerializerMethodField()

    def get_author_list(self, book_obj):
        # 拿到queryset開始循環 [{}, {}, {}, {}]
        authors = list()

        for author in book_obj.authors.all():
            authors.append(author.name)

        return authors

步驟如下:

  • 繼承ModelSerializer:不再繼承Serializer

  • 添加extra_kwargs類變量:extra_kwargs = {‘publish’: {‘write_only’: True}}

  註:本篇博客參考自:https://pizzali.github.io/

DRF之解析器組件及序列化組件