DRF之解析器組件及序列化組件
阿新 • • 發佈:2018-12-12
除了 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 #View Code判斷a的真假值,如果為True,則將判斷表達式的前面的值賦給c,否則將判斷表達式後面的值賦給c c = a if a else b print(c) # 2 # 因為a的真假值判斷為False,所以c為2
知識點復習回顧二:列表推導式
列表推導式的返回結果仍然是一個列表,假設有列表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 viewView 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 viewView 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(r‘books/$‘, 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之解析器組件及序列化組件