APIView和序列化類 serializers

小满三岁啦發表於2024-04-11

常規透過CBV的寫法

# models.py
from django.db import models

class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.IntegerField()
    publish = models.CharField(max_length=64)
    
    class Meta:
        db_table = "book"
# urls.py
from django.urls import path
from .views import BookView, BookDetailView


urlpatterns = [
    # 查詢全部圖書以及新增圖書
    path("books/", BookView.as_view()),
    # 修改一本圖書,刪除一本圖書,查詢1本圖書
    path("books/<int:pk>/", BookDetailView.as_view())
]
import json
from urllib.request import unquote
from django.shortcuts import render
from .models import Book
from django.http import JsonResponse
from django.views import View


# 127.0.0.1:8000/app01/books/
class BookView(View):
    # 查詢全部書籍
    def get(self, request):
        book_obj = Book.objects.all()

        data_list = []
        
        for obj in book_obj:
            data_list.append({"name": obj.name, "price": obj.price, "publish": obj.publish})
        
        print(data_list)
        return JsonResponse({"code": 100, "msg": "查詢成功!", "results": data_list})

    # 新增一本書籍
    def post(self, request):
        data = request.POST
        Book.objects.create(name=data.get("name"), price=data.get("price"), publish=data.get("publish"))
        return JsonResponse({"code": 100, "msg": "新增成功!"})
    
    
# 127.0.0.1:8000:app01/books/pk/
class BookDetailView(View):
    # 修改單本書籍
    def put(self, request, pk):
        # 這裡獲取到透過二進位制的字串  b'%..'
        body = request.body
        
        # print("進來的資料是json格式的") 即前端是透過json傳送請求的
        data_dict = json.loads(body)
        Book.objects.filter(pk=pk).update(**data_dict)
        
        # 因為透過PUT方法傳送的請求並不會在POST裡面,所以只能在request.body裡面取資料
        # 但是這個資料是 b'%..'的形式,所以我們需要透過unquote去轉碼成正常的資料,然後再去運算元據庫
        data = unquote(body)  # name=挪威的森林&price=88&publish=上海出版社
        data_dict = {k: v for k, v in (line.split("=") for line in data.split("&"))}
        
        # 更新資料到資料庫裡面
        Book.objects.filter(pk=pk).update(**data_dict)
        return JsonResponse({"code": 100, "msg": "修改成功!"})
    
    # 刪除單本書籍
    def delete(self, request, pk):
        Book.objects.filter(pk=pk).delete()
        return JsonResponse({"code": 100, "msg": "刪除成功!"})
    
    
    # 查詢單本書籍
    def get(self, request, pk):
        book_obj = Book.objects.filter(pk=pk).first()
        data = {"name": book_obj.name, "price": book_obj.price, "publish": book_obj.publish}
        return JsonResponse({"code": 100, "msg": "查詢成功!", "result": data})

透過傳統檢視寫有什麼問題

  1. 取資料不友好,資料有時候能從request.POST取出來,又時候又只能從request.body裡面取出來資料,而且是同一個介面,所以會加大後端程式碼的冗餘。
  2. 傳送POST請求會遇到403Forbidden ,需要每一次都解決csrf的問題
  3. 有沒有一個方案即能從某個指定的地方拿到資料不需要轉格式,又能幫我們解決csrf_token的問題呢?
  4. 可以使用APIView

request.content_type

可以用來獲取前端的請求頭編碼型別,參考下表:

請求編碼型別 request.content_type列印結果
form-data multipart/form-data; boundary=--------------------------109480859847807332950202
urlencoded application/x-www-form-urlencoded
json application/json

透過content_type,獲取到編碼型別之後,在APIView中去判斷就更方便處理了。

基於APIView編寫五個介面

#  繼承APIView
from rest_framework.views import APIView
# 基於上面的問題以及restful規範,新開一個路徑v1 然後使用APIView的方式重寫一遍,解決上面的問題


# views.py
# =-------------------- 下面是基於 APIView 寫的五個方法
from rest_framework.views import APIView

# 127.0.0.1:8000/app01/v1/books/
class BookViewV1(APIView):
    # 查詢全部書籍
    def get(self, request):
        book_obj = Book.objects.all()

        data_list = []
        
        for obj in book_obj:
            data_list.append({"name": obj.name, "price": obj.price, "publish": obj.publish})
            
        return JsonResponse({"code": 100, "msg": "查詢成功!", "results": data_list})

    # 新增一本書籍
    def post(self, request):
        items = request.data
        if request.content_type == 'application/json':
            # 這裡可以透過request.content_type 去判斷前端的編碼型別 目前這個縮排是json的結果,能直接拿到資料
            # 如果前端是透過json方式傳過來的資料,那麼拿到的就直接是一個字典
            data = items
        else:
            # 否則 得到的就是 QueryDict 需要透過.get取值
            data = {"name": items.get("name"), "price": items.get("price"), "publish": items.get("publish")}
        Book.objects.create(name=data.get("name"), price=data.get("price"), publish=data.get("publish"))
        return JsonResponse({"code": 100, "msg": "新增成功!"})
    

# 127.0.0.1:8000/app01/v1/books/pk/
class BookDetailViewV1(APIView):
    # 修改單本書籍
    def put(self, request, pk):
        items = request.data
        if request.content_type == 'application/json':
            data = items
        else:
            data = {"name": items.get("name"), "price": items.get("price"), "publish": items.get("publish")}
        # 更新資料到資料庫裡面
        Book.objects.filter(pk=pk).update(**data)
        return JsonResponse({"code": 100, "msg": "修改成功!"})
    
    # 刪除單本書籍
    def delete(self, request, pk):
        Book.objects.filter(pk=pk).delete()
        return JsonResponse({"code": 100, "msg": "刪除成功!"})
    
    
    # 查詢單本書籍
    def get(self, request, pk):
        book_obj = Book.objects.filter(pk=pk).first()
        data = {"name": book_obj.name, "price": book_obj.price, "publish": book_obj.publish}
        return JsonResponse({"code": 100, "msg": "查詢成功!", "result": data})
# urls.py
from django.urls import path
from .views import BookViewV1, BookDetailViewV1


urlpatterns = [
    # ---------- 下面是基於 APIView 去寫的 -----------------
    path("v1/books/",BookViewV1.as_view()),
    path("v1/books/<int:pk>/", BookDetailViewV1.as_view())
]
# modes.py 不變

APIView的執行流程(難)重要

# 1 APIView繼承了 Django的View---》 class APIView(View)

# 2 請求來了,路由匹配成功後---》執行流程
	2.1 路由配置 path('books/', BookView.as_view()),
    2.2 BookView.as_view()(request)-->BookView中沒有as_view--》找父類APIView的as_view
    	BookView-->APIView-->View
        
    2.3 APIView的as_view
        @classmethod # 繫結給類的方法,類來呼叫
        def as_view(cls, **initkwargs):
            # super代指父類--》父類是View--》之前讀過--》self.dispatch()
            # 這個view 還是原來View的as_view的執行結果--》as_view中有個view內部函式
            view = super().as_view(**initkwargs)
            # 只要繼承了APIView,不需要處理csrf
            '''
            @csrf_exempt
            def index(request):
            	pass
           	等同於  index=csrf_exempt(index)
           	以後呼叫index,其實呼叫的 是csrf_exempt(index)()
            '''
            return csrf_exempt(view)
     2.4 請求來了,真正執行的是:csrf_exempt(view)(request)-->去除了csrf的view(request)--》self.dispatch()
     2.5 請求來了,真正執行的是 self.dispatch(request)--->self 是 檢視類的物件
         BookView的物件--》自己沒有--》APIView中
     2.6 現在要看 APIView的dispatch
     def dispatch(self, request, *args, **kwargs):
        # 1 包裝了新的request物件---》現在這個requets物件,已經不是原來django的request物件了
        request = self.initialize_request(request, *args, **kwargs)
        try:
            # 2 APIView的initial--》三件事:三大認證:認證,頻率,許可權
            self.initial(request, *args, **kwargs)
            # 3 就是執行跟請求方式同名的方法
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
			# 4 如果在三大認證或檢視類的方法中出了異常,會被統一捕獲處理
        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
 # 執行流程總結
	1 只要繼承了APIView,就沒有csrf限制了
    2 只要繼承了APIView,request就是新的request了,它有data
    3 在執行跟請求方式同名的方法之前,執行了三大認證:認證,頻率,許可權
    4 只要在三大認證或者檢視類的方法中出了一場,都會被捕獲,統一處理

Request物件

#1 APIView執行流程---》request物件---》變成了新的
	-多了 request.data
#2 新的Request具體是哪個類的物件
	rest_framework.request.Request 類的物件
#3 老的request是哪個類的物件
	django.core.handlers.wsgi.WSGIRequest
	
# 4 老的request可以
	-request.method
    -request.path
    -request.META.get('REMOTE_ADDR')
    -request.FILES.get()
    ...
# 5 新的request支援之前所有老request的操作
	-1 之前如何用,還是如何用
    -2 request.data-->請求體的資料--》方法包裝成了資料屬性
       @property
        def data(self):
            if not _hasattr(self, '_full_data'):
                self._load_data_and_files()
            return self._full_data
    -3 request.query_params--->原來的request.GET--》貼合restful規範-》
        @property
        def query_params(self):
            return self._request.GET
    
    
    -4 request._request 就是老的request
    
    
# 6 原始碼分析---》為什麼:之前如何用,還是如何用?沒有繼承關係
	from rest_framework.request import Request
    -__getattr__: .攔截方法,物件.屬性,如果屬性不存在,就會觸發__getattr__執行
    -requst.method -->新的request沒有--》會觸發新的Request類中的 __getattr__
        def __getattr__(self, attr):
            try:
                # 根據字串 _request 獲取self中的屬性
                # _request 就是原來老的request
                _request = self.__getattribute__("_request")
                # 透過反射,去老的request中,獲取屬性
                return getattr(_request, attr)
            except AttributeError:
                return self.__getattribute__(attr)
            
# 總結:記住的 新的request
	-1 之前如何用,還是如何用
    -2 request.data-->請求體的資料--》方法包裝成了資料屬性
    -3 request.query_params--->原來的request.GET--》貼合restful規範-》
    -4 request._request 就是老的request
	-5 魔法方法之 __getattr__

APIView到底新增了哪些方法?

方法 說明
request.data 如果是json方式直接拿到資料字典,如果是from-data或者urlencoded需要自己再次處理
request.query_params 還是原來的request.GET--》貼合restful規範-》 推薦後續GET方法使用
request._request 就是原來的request

魔法方法之 __getattr__

# 以__開頭  __結尾的都是魔法方法,魔法方法並不是我們主動呼叫的,而是某種情況下,自動去觸發的。

# __getattr__ 為攔截方法,如果物件.屬性 不存在 就會觸發該方法的執行。
class Hero:
    
    def __getattr__(self, name):
        print(f"根據 {name} 去取值")
        return "我是預設值 3"
    
    
hero = Hero()
hero.name = "小滿"  # 如果屬性存在 正常列印, 如果屬性不存在正常情況下會報錯,不過定義了__getattr__ 方法會自動觸發
print(hero.age) # 根據這裡的屬性
# 根據 age 去取值
# 我是預設值 3

序列化類 serializer

虛擬碼

# BookSerializer.py 
from rest_framework import serializers  # 
class BookSerializer(serializers.Serializer):
    # 下面寫欄位
    ...
   
# views
from .serializer import BookSerializer  # 自己定義的類
from rest_framework.response import Response
from rest_framework.views import APIView # 不繼承APIView也可以 怎麼方便怎麼來

class 類名(APIView):
    # 序列化多個物件
    obj_list = 模型層的類.objects.all()
    # 序列化多個類 加上many=True
    serializer = BookSerializer(instance=obj_list, many=True)
    
    # ---- 下面是序列化單個
    obj = 模型層的類.objects.filter(pk=pk).first()
    serializer = BookSerializer(instance=obj)
    
    # 注意,返回的時候,我們也不使用JsonResponse了,使用def的Response
    return Response({"code": 100, "msg": "ok"})

引入

# 上面,我們已經透過APIView 最佳化了5個介面,不過還是有許多問題:
 - 做序列化的時候,手動去做,方法很笨。
 - 擴充性差,後續如果要序列化某個、或少序列化某個欄位,會比較麻煩

# 最佳化 藉助 drf 提供的序列化類,有下面的優點:
	- 1. 可以幫助我們快速的完成序列化
    - 2. 資料校驗,幫助我們反序列化之前校驗資料的準確性
    - 3. 可以幫助我們做反序列化
    
# 如何使用?
	- 1. 新建一個py檔案,名稱隨意,在裡面新建一個類,然後繼承Serialier
    - 2. 在類中寫欄位,欄位就是要序列化的欄位
    - 3. 在檢視函式中,序列化類,例項化得到物件,傳入該傳的引數
    	- 單條
        - 多條
    - 4. 呼叫序列化類物件的 serializer.data 方法完成序列化

序列化案例 查詢相關

# serializer.py 自己建立的Py檔案
from rest_framework import serializers


class BookSerializer(serializers.Serializer):
    # 注意注意, 這裡指定欄位型別需要透過  serializers 
    name = serializers.CharField()
    price = serializers.IntegerField()
    publish = serializers.CharField()
# 重開一個路徑v2 使用serializers 做進一步最佳化
# 128.0.0.0:8000/app01/v2/books/

# views.py
# ------------------------------ 下面是基於  serializrs 序列化
from .serializer import BookSerializer
from rest_framework.response import Response

# 127.0.0.1:8000/app01/v2/books/
class BookViewV2(APIView):
    def get(self, request):
        obj_list = Book.objects.all()
        # 要序列化的qs物件,但是如果是很多條資料,必須加上 many=True   預設的情況下 instance=None data=empty
        serailizer = BookSerializer(instance=obj_list, many=True)
        # 需要注意的是,這裡不用使用JsonResponse了,而是使用drf的Response去返回
        # from rest_framework.response import Response
        return Response({"code": 100, "msg": "成功!", "results": serailizer.data})

# 127.0.0.1:8000/app01/v2/books/pk/
class BookDetailViewV2(APIView):
    def get(self, request, pk):
        obj = Book.objects.filter(pk=pk).filter().first()
        # 因為這裡是查詢單本書的介面,所以不需要傳入many引數,如果一定要傳 傳入many=False即可,預設也是many=False
        serializer = BookSerializer(instance=obj, many=False)
        return Response({"code": 100, "msg": "查詢成功!", "result": serializer.data})

資料校驗案例 post (校驗前端傳入的資料和forms很像)

  1. 主要功能是校驗前端傳入的資料
  2. 資料校驗和反序列化的時候,不能傳入instance,而要傳入data
  3. 在 Django REST Framework 中,每個欄位對應的驗證方法可以透過 validate_<field_name> 的方式來定義,其中 <field_name> 是欄位的名稱。
# serializer.py
from rest_framework import serializers
from rest_framework.exceptions import ValidationError


class BookSerializer(serializers.Serializer):
    # 注意注意, 這裡指定欄位型別需要透過  serializers 
    name = serializers.CharField(max_length=10, min_length=3)  # 注意注意! 這裡的length指的是字元的長度 ,不是編碼!!
    price = serializers.IntegerField(max_value=200, min_value=20) # 最大值和最小值 
    publish = serializers.CharField()
    
    # 區域性鉤子,可以給某一個欄位指定條件
    # name
    # name中不能包含sb開頭活著結尾
    def validate_name(self, name):
        if "sb" in name:
            # 不合法,丟擲異常
            raise ValidationError("書名不合法!")
        # 書名如果合法,需要返回出去 
        return name
    
    # 全域性鉤子, 多個欄位校驗
    # 要求,書名不能和出版社名稱相同
    def validate(self, attrs):
        # attrs [字典]
        # 這個attrs 是什麼,這個attrs 就是透過自己校驗,以及透過區域性鉤子校驗透過之後的資料,然後才走到全域性鉤子
        if attrs.get("name") == attrs.get("publish"):
            raise ValidationError("書名不能和出出版社名稱一樣")
        
        # 不要忘記返回
        return attrs

# views.py
from .serializer import BookSerializer
from rest_framework.response import Response

# 127.0.0.1:8000/app01/v2/books/
class BookViewV2(APIView):
    def post(self, request):
        # data獲取資料
        # 1 校驗前端傳入的資料
        # 2 資料校驗和反序列化 --->  這裡不能傳入instance 而要傳入data
        serializer = BookSerializer(data=request.data)
        # 3 進行資料的校驗
        if serializer.is_valid():
            # 校驗透過,儲存
            return Response({"code": 100, "msg": "儲存成功!騙你的 略~"})
        else:
            # 資料校驗不透過,主動丟擲異常,使用serializer.errors
            return JsonResponse({"code": 100, "msg": serializer.errors})

資料校驗透過儲存資料

檢視層:save()

自定義序列化類:

  1. 如果是put,方式進來,即修改資料,重寫update方法
  2. 如果是post,方式進來,即新增資料,重新create方法
# 自定義序列化類.py
from rest_framework import serializers
from .models import Book
from rest_framework.exceptions import ValidationError


class BookSerializer(serializers.Serializer):
	# ...
	# 刪除的部分同之前一樣
    

    # validated_data 固定寫法
    # 一般post重寫此方法
    def create(self, validated_data):
        # validated_data  這個是前端透過校驗的資料
        book = Book.objects.create(**validated_data)
        # 別忘記返回出去
        return book
    
    # 一般put請求重寫此方法
    def update(self, instance, validated_data):
        # instance 要修改的物件
        # validated_data  資料
        
        # 本辦法 待最佳化
        # instance.name = validated_data.get("name")
        # instance.price = validated_data.get("price")
        # instance.publish = validated_data.get("publish")
        
        # 最佳化後的方法,透過反射
        for k, v in validated_data.items():
            setattr(instance, k, v)
        
        # 別忘記save()
        instance.save()
        
        # 儲存之後,記得返回給前端
        return instance
        
# 127.0.0.1:8000/app01/v2/books/
class BookViewV2(APIView):
    def post(self, request):
        # data獲取資料
        # 1 校驗前端傳入的資料
        # 2 資料校驗和反序列化 --->  這裡不能傳入instance 而要傳入data
        serializer = BookSerializer(data=request.data)
        # 3 進行資料的校驗
        if serializer.is_valid():
            # 校驗透過,儲存
            serializer.save()  # 序列化類要重寫create方法
            return Response({"code": 100, "msg": "儲存成功!騙你的 略~"})
        else:
            # 資料校驗不透過,主動丟擲異常,使用serializer.errors
            return JsonResponse({"code": 100, "msg": serializer.errors})
        

# 127.0.0.1:8000/app01/v2/books/pk/
class BookDetailViewV2(APIView):
    # 修改資料
    def put(self, request, pk):
        obj = Book.objects.filter(pk=pk).first()
        items = request.data
        if request.content_type == 'application/json':
            data = items
        else:
            data = {"name": items.get("name"), "price": items.get("price"), "publish": items.get("publish")}
        
        # 注意!!! 改物件必須傳instance和data
        serializer = BookSerializer(instance=obj, data=request.data)
        if serializer.is_valid():
            serializer.save()  # 記得自定義序列化類裡面要重寫update方法
            return Response({"code": 100, "msg": "更新成功!"})
        else:
            return Response({"code": 100, "msg": serializer.errors})

異常和設定中文返回異常

image-20240411194834366

主動丟擲異常 serializer.errors (一般寫在檢視中)

{
    "code": 100,
    "msg": {
        "name": [
            "Ensure this field has at least 3 characters."
        ]
    }
}

異常類基類 ValidationError (一般寫在自定義的序列化類中)

from rest_framework.exceptions import ValidationError

raise ValidationError("資料錯誤!")

如果想要異常修改為中文,需要在settings中去設定下面的操作

# 註冊app, app的名稱 rest_framework

INSTALLED_APPS = [
    "rest_framework",  # 這個
]

LANGUAGE_CODE = "zh-hans"
TIME_ZONE = "Asia/Shanghai"
USE_TZ = False

# 然後就可以顯示中文的結果了

{
    "code": 100,
    "msg": {
        "name": [
            "請確保這個欄位至少包含 3 個字元。"
        ]
    }
}

如果包含了多個錯誤,那麼結果也會是多個錯誤

{
    "code": 100,
    "msg": {
        "name": [
            "請確保這個欄位至少包含 3 個字元。"
        ],
        "price": [
            "請確保該值小於或者等於 200。"
        ]
    }
}

回顧反射的相關知識

更詳細的可以看看這篇文章:https://hiyongz.github.io/posts/python-notes-for-reflection/

hasattr

返回布林值,有返回值

class Hero:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age 
        
xm = Hero(name="小滿", age=3)

print(hasattr(xm, "hobby"))  # False
print(hasattr(xm, "name"))   # True

getattr

參考字典的get即可,有返回值

class Hero:

    def __init__(self, name, age):
        self.name = name
        self.age = age 
        
        
xm = Hero(name="小滿", age=3)

print(getattr(xm, "name", "None"))  # 小滿
print(getattr(xm, "hobby", "None"))  # None

setattr

動態設定屬性,無返回值

class Hero:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age 
        
xm = Hero(name="小滿", age=3)

setattr(xm, "hobby", "摸魚")
setattr(xm, "name", "大喬")

print(xm.__dict__)

# {'name': '大喬', 'age': 3, 'hobby': '摸魚'}

delattr

有則刪除,無則報錯,無返回值

class Hero:

    def __init__(self, name, age):
        self.name = name
        self.age = age 
        
        
xm = Hero(name="小滿", age=3)

delattr(xm, "name")
# delattr(xm, "hobby")  # AttributeError: hobby

print(xm.__dict__)  # {'age': 3}

自己寫裝飾器實現request.data

import re
from dataclasses import dataclass

@dataclass
class ToDict:
    f: callable
    
    def __call__(self, request, *args, **kwargs):
        base_dict = {}
        
        # 檢查請求的內容型別
        if request.content_type == "application/json":
            # 如果是 JSON 格式的請求體,根據請求方法解析資料
            if request.method.lower() in ("post", "put"):
                base_dict.update(json.loads(request.body))
            else:
                # 否則,解析表單資料
                body = unquote(request.body)
                data = {k: v for k, v in (item.split("=") for item in body.split("&"))}
                base_dict.update(data)
        else:
            # 如果是表單型別的請求
            if request.method.lower() == 'get':
                # 解析 GET 請求的引數
                for key, value in request.GET.items():
                    base_dict[key] = value
            elif request.method.lower() == 'post':
                # 解析 POST 請求的引數
                for key, value in request.POST.items():
                    base_dict[key] = value
            else:
                # 如果是其他型別的請求
                if request.content_type == 'application/x-www-form-urlencoded':
                    # 解析表單資料
                    items = unquote(request.body)
                    data = {k: v for k, v in (item.split("=") for item in items.split("&"))}
                    base_dict.update(data)
                else:
                    # 使用正規表示式解析其他型別的資料
                    items = ''.join(unquote(request.body).split())
                    string = re.findall(r'.*?name="(.*?)"(.*?)-.*?', items, re.S | re.I)
                    for line in string:
                        k, v = line
                        base_dict[k] = v
        
        # 列印解析的資料
        print(base_dict)
        
        # 呼叫原始函式並返回結果
        return self.f(request)
            
    
    

@ToDict
def book_view_v3(request):
    return JsonResponse({"code": 100, "msg": "成功!"})

相關文章