常規透過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})
透過傳統檢視寫有什麼問題
- 取資料不友好,資料有時候能從request.POST取出來,又時候又只能從request.body裡面取出來資料,而且是同一個介面,所以會加大後端程式碼的冗餘。
- 傳送POST請求會遇到403Forbidden ,需要每一次都解決csrf的問題
- 有沒有一個方案即能從某個指定的地方拿到資料不需要轉格式,又能幫我們解決csrf_token的問題呢?
- 可以使用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很像)
- 主要功能是校驗前端傳入的資料
- 資料校驗和反序列化的時候,不能傳入instance,而要傳入data
- 在 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()
自定義序列化類:
- 如果是put,方式進來,即修改資料,重寫update方法
- 如果是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})
異常和設定中文返回異常
主動丟擲異常 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": "成功!"})