1. 程式人生 > >DRF之檢視元件

DRF之檢視元件

不斷的優化我們寫的程式,是每個程式設計師必備的技能和職業素養,也是幫助我們成長的非常重要的手段。

引入

通過上一節課的學習,我們已經通過DRF的序列化元件,設計出了符合rest規範的GET、POST介面邏輯,我們知道操作資料庫主要有增刪改查幾種方式,增加,查詢兩種介面邏輯我們都實現完畢了,接下來,我們繼續實現剩下的兩種,刪除和修改。

今日概要

  • serializer進行put、delete及獲取單條資料的介面設計
  • 檢視元件的使用及原始碼剖析

知識點複習回顧

  • 昨日回顧
  • RESTful api介面規範
  • 混入類、多繼承
  • 函式的引數

在開始之前,按照慣例,我們複習前兩節課學習過的知識點、以及學習今天的新知識點所需掌握的知識點。

知識點複習回顧一:RESTful api介面規範

第一節課的那張圖大家還有印象嗎?如果沒有印象,請同學們思考一下,我給大家總結的REST的最重要的一句話,那就是:url用來唯一定位資源,http請求方式用來定位使用者行為。

根據這句話,我們設計了下面的RESTful api:

view

上面就是關於REST規範的回顧,請牢記:url用來唯一定位資源,http請求方式用來定位使用者行為。

知識點複習回顧二:混入類、多繼承

我有一個Animal類,它包含如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Animal(object):
def eat(self):
print("Eat")

def walk(self):
print("Walk")

def sleep(self):
print("Sleep")

def run(self):
print("Run")

def flying(self):
print("Flying")

def wangwang(self):
print("Wangwang")

def miao(self):
print()


class Dog(Animal):pass


class Cat(Animal):pass


class Bird(Animal):pass

可以看到,Dog類繼承了Animal類,但是Dog並沒有飛行和喵的功能,所以,如果直接繼承Animal會有一些問題,請同學們思考,如何解決這個問題呢?

好了,其實我們有多中方式可以解決這個問題,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Animal(object):
def eat(self):
print("Eat")

def walk(self):
print("Walk")

def sleep(self):
print("Sleep")

def run(self):
print("Run")


class Flying(object):
def flying(self):
print("Flying")


class WangWang(object):
def wangwang(self):
print("Wangwang")


class Miao(object):
def miao(self):
print()


class Dog(Animal, WangWang):pass


class Cat(Animal, Miao):pass


class Bird(Animal, Flying):pass

我們將不同的功能封裝到了獨立的類中,然後採用一種Mixin(混合類)的方式,其實也就是多繼承,來解決這個問題,這在Python中是比較常見的解決方式,比如socketserver模組就用到了這種方式,當我們需要執行緒的時候,可以繼承執行緒類,當我們需要程序的時候,可以繼承程序類。

知識點複習回顧三:函式的引數

接下來,我們一起回顧一下函式的引數,假設我有函式如下:

1
2
3
4
5
6
7
8
def func(a, b, c=1, *args, **kwargs):
print(a, b, c, args, kwargs)

func(1, 2, 3, 4, 5) # 1, 2, 3, (4, 5) 所有未被匹配到的非key=value型的引數都會被*args接收
func(b=1, a=1, c=2, 4, 5) # 報錯,位置引數不能在關鍵字引數之後
func(4, 5, c=3, {"name": "pizza"}) # 報錯,位置引數不能在關鍵字引數之後,字典不是關鍵字引數
func({"name": "pizza"}, 1, 2) # {'name': 'pizza'} 1 2 () {}
func(1, 2, 3, 4, 5, d=1, e=2) # 1 2 3 (4, 5) {'d': 1, 'e': 2} 未被匹配到的關鍵字引數被傳遞給kwargs

今日詳細

好了,知識點的複習和補充,咱們就到這裡,接下來,正式開始今天的內容,首先開始設計剩下三個介面邏輯。

使用serializer進行put介面設計

根據規範,PUT介面用來定義使用者對資料修改的邏輯,也就是update,它的url是這樣的, 127.0.0.1/books/1/,請求方式是PUT,1代表的是具體的資料,使使用者動態傳遞的,所以我們的url應該是這樣的:

1
re_path(r'books/(\d+)/$', views.BookFilterView.as_view()),

此時我們應該重新定義一個檢視類,因為url不一樣了,所以在views.py中,需新增一個檢視類:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from rest_framework.views import APIView
from app_serializer import BookSerializer


class BookFilterView(APIView):

def put(self, request, nid):
book_obj = Book.objects.get(pk=nid)

serialized_data = BookSerializer(data=request.data, instance=book_obj)

if serialized_data.is_valid():
serialized_data.save()
else:
return Response(serialized_data.errors)

請注意,在序列化時,我們除了傳入data引數外,還需告訴序列化元件,我們需要更新哪條資料,也就是instance,另外,我們使用的序列化類還是之前那個:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

使用POSTMAN工具傳送一個PUT請求修改資料: PUT http://127.0.0.1:9001/serializers/books/1

請注意,此時會報錯:RuntimeError: You called this URL via PUT, but the URL doesn’t end in a slash and you have APPEND_SLASH set. Django can’t redirect to the slash URL while maintaining PUT data. Change your form to point to 127.0.0.1:9001/serializers/books/1/ (note the trailing slash), or set APPEND_SLASH=False in your Django settings.

因為,如果是GET請求,Django的全域性APPEND_SLASH引數為True,所以會在url後面加上/(如果沒有),但是如果是PUT或者DELETE請求,APPEND_SLASH不會新增 / 到url末尾。而我們上面定義的url是明確以 / 結尾的,所以,我們應該在url後面加上反斜線 / ,或者把url修改為不以斜線結尾。

加上之後,再次傳送請求修改資料:PUT http://127.0.0.1:9001/serializers/books/1/,檢視資料庫,發現,資料已經被修改了。

這就是PUT介面邏輯的設計,分為如下幾個步驟:

  • url設計:re_path(r’books/(\d+)/$’, views.BookFilterView.as_view())
  • 檢視類:重新定義一個檢視類
  • put方法:在檢視類中定義一個put方法
  • 序列化:在序列化的過程中,需要傳入當前修改的資料行,引數名為instance
  • 序列化類:不需要修改
  • url路徑:請求時,傳送的url必須與urls.py中定義的url完全匹配
使用serializer進行delete介面設計

接下來,繼續設計delete介面,根據規範,delete介面的url為:127.0.0.1/books/1/,請求方式是DELETE,與put是一致的,都是對使用者指定的某行資料進行操作,數字1是動態的,所以我們的url不變:

1
re_path(r'books/(\d+)/$', views.BookFilterView.as_view()),

同樣的,檢視類和序列化類都不需要重新定義,只需要在檢視類中定義一個delete方法即可,如下程式碼所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class BookFilterView(APIView):

def delete(self, request, nid):
book_obj = Book.objects.get(pk=nid).delete()

return Response("")

def put(self, request, nid):
book_obj = Book.objects.get(pk=nid)

serialized_data = BookSerializer(data=request.data, instance=book_obj)

if serialized_data.is_valid():
serialized_data.save()
return Response(serialized_data.data)
else:
return Response(serialized_data.errors)

用POSTMAN來試試DELETE請求:http://127.0.0.1:9001/serializers/books/53/,我們將剛剛新增的資料刪除,操作成功,同樣的,請注意,請求url必須完全匹配urls.py中定義的url。

使用serializer進行單條資料的介面設計

最後一個介面的設計,是對單條資料進行獲取,根據規範url為:127.0.0.1/books/1/,請求方式為GET,根據url和前面兩個介面的經驗,這次仍然使用之前的檢視類,因為根據REST規範,url唯一定位資源,127.0.0.1/books/1/和127.0.0.1/books/是不同的資源,所以,我們不能使用之前那個獲取全部資料的檢視類。這裡肯定不能重用之前的那個get方法,必須重新定義一個get方法。

urls.py不變,新增三個介面邏輯後的檢視類如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class BookFilterView(APIView):
def get(self, request, nid):
book_obj = Book.objects.get(pk=nid)

serialized_data = BookSerializer(book_obj, many=False)

return Response(serialized_data.data)

def delete(self, request, nid):
book_obj = Book.objects.get(pk=nid).delete()

return Response("")

def put(self, request, nid):
book_obj = Book.objects.get(pk=nid)

serialized_data = BookSerializer(data=request.data, instance=book_obj)

if serialized_data.is_valid():
serialized_data.save()
return Response(serialized_data.data)
else:
return Response(serialized_data.errors)

many=False, 當然,也可以不傳這個引數,因為預設是False。通過POSTMAN傳送請求,成功。三個介面定義完成了,加上上一節課的get和post,兩個檢視類的介面邏輯如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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()
return Response(verified_data.data)
else:
return Response(verified_data.errors)


class BookFilterView(APIView):
def get(self, request, nid):
book_obj = Book.objects.get(pk=nid)

serialized_data = BookSerializer(book_obj, many=False)

return Response(serialized_data.data)

def delete(self, request, nid):
book_obj = Book.objects.get(pk=nid).delete()

return Response("")

def put(self, request, nid):
book_obj = Book.objects.get(pk=nid)

serialized_data = BookSerializer(data=request.data, instance=book_obj)

if serialized_data.is_valid():
serialized_data.save()
return Response(serialized_data.data)
else:
return Response(serialized_data.errors)

到此為止,我們已經通過序列化元件設計出了符合REST規範的五個常用介面,已經足夠優秀了,但是還不夠完美,現在假設,我們有多個數據介面,比如(Book,Author,Publish…..)等資料表都需要定義類似的介面,可以預見,我們需要重複定義類似上面的五個介面,這種方式將會導致大量的重複程式碼出現,顯然,我們的程式還有很多可以優化的地方。

請同學們思考,如果是你,你將會如何進行優化呢?

好了,同學們,結合剛剛上課是給大家回顧的混合類和多繼承,我們是否可以使用下面的方式呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class GetAllData():
def get(self, request):pass


class GetOneData():
def get(self, request, nid):pass


class DeleteOneData():
def delete(self, request, nid):pass


class UpdateOneData():
def put(self, request, nid):pass


class CreateData():
def post(self, request):pass


class BookView(APIView, GetAllData, CreateData):pass


class BookFilterView(APIView, GetOneData, DeleteOneData, UpdateOneData):pass

將每個介面都寫到獨立的類中,然後使用多繼承,或者成為mixin的這種方式,就可以對我們的程式進行優化,mixin的方式非常常見,在學網路程式設計的時候,如果你看過socketserver原始碼,就會發現,socketserver中就有對mixin的實現,即,假設我們需要程序的時候,我們繼承程序類,如果我們需要執行緒的時候,我們繼承執行緒類即可。

接下來,我們一起來看看DRF是如何做的,其他,它的解決方式與我們上面的方式的思路是一樣的。

使用mixin優化介面邏輯

mixin的使用方式介紹

urls.py有些區別:

1
2
3
4
5
6
7
8
from django.urls import re_path

from mixiner import views

urlpatterns = [
re_path(r'books/$', views.BookView.as_view()),
re_path(r'books/(?P<pk>\d+)/$', views.BookFilterView.as_view()),
]

views.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# -*- coding: utf-8 -*-
from rest_framework.mixins import (
ListModelMixin,
CreateModelMixin,
UpdateModelMixin,
DestroyModelMixin,
RetrieveModelMixin
)
from rest_framework.generics import GenericAPIView

# 當前app中的模組
from .models import Book
from mixin_serializer import BookSerializer

# Create your views here.


class BookView(ListModelMixin, CreateModelMixin, GenericAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer

def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)

def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)


class BookFilterView(DestroyModelMixin,
UpdateModelMixin,
RetrieveModelMixin,
GenericAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer

def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)

def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)

def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)

DRF將這五個介面定義在不同的ModelMixin中,使用步驟如下:

  • 匯入ModelMixin
  • 檢視類繼承所需的ModelMix
  • 不再繼承APIView,需要繼承generics.GenericAPIView
  • 必須包含兩個類變數:queryset,serializer_class
  • 介面中不需要定義任何邏輯操作,一切交給mixin
  • 每個介面的返回值不同,對應關係:{“get”: “list”, “delete”: “destroy”, “put”: “update”, “get”: “retrieve”, “post”: “create”}

其中有兩個get方法,但是分屬於不同的檢視類,請注意在url中的不同點,因為我們統一給的都是QuerySet,所以,需要通過傳入一個名為pk的命名引數,告訴檢視元件,使用者需要操作的具體資料。

mixin原始碼剖析

到底它是如何做的呢?我們來簡單剖析一下原始碼:

  • Django程式啟動,開始初始化,讀取urls.py, 讀取settings, 讀取檢視類
  • 執行as_views(), BookView沒有,需要到父類中找
  • 幾個ModelMixin也沒有,GenericAPIView中沒有,繼續到GenericAPIView(APIView)中找
  • 找到了,並且與之前的邏輯是一樣的,同時我們發現GenericAPIView中定義了查詢queryset和serializer_class類的方法
  • as_view()方法返回重新封裝的檢視函式,開始建立url和檢視函式之間的對映關係
  • 等待使用者請求
  • 接收到使用者請求,根據url找到檢視函式
  • 執行檢視函式的dispatch方法(因為檢視函式的返回值是:return self.dispatch()
  • dispatch分發請求,查詢到檢視類的五個方法中的某個
  • 開始執行,比如post請求,返回:self.create(),檢視類本身沒有,則會到父類中查詢
  • 最後在CreateModelMixin中查詢
  • 執行create()方法,獲取queryset和serializer_class
  • 返回資料

在對單條資料進行操作的幾個方法裡面,比如retrieve,會執行get_object()方法,該方法會根據lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field來查詢操作物件,我們可以通過修改self.lookup_url_kwarg變數名來自定義引數。

好了,以上就是mixin的原始碼剖析。

使用view優化介面邏輯

view的使用方式介紹

看似已經優化的非常完美了,但是,在一個對效能要求極高的專案裡面,我們的程式還可以繼續優化,還需要繼續優化,不斷的優化我們寫的程式,是每個程式設計師必備的技能,也是幫助我們成長的非常重要的手段。同樣的思路,同樣的方式,我們可以將多個介面封裝到一個功能類中,請看下面的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# -*- coding: utf-8 -*-
from rest_framework import generics

# 當前app中的模組
from .models import Book
from .serializer_classes import BookSerializer

# Create your views here.


class BookView(generics.ListCreateAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer


class BookFilterView(generics.RetrieveUpdateDestroyAPIView):
queryset = Book.objects.all()
serializer_class = BookSerializer

是不是非常簡單,你想到了嗎?

使用viewset優化介面邏輯

這樣就結束了嗎?哈哈哈,還是那句話,看似已經優化的非常完美了,但是,在一個對效能要求極高的專案裡面,我們的程式還可以繼續優化,還需要繼續優化,不斷的優化我們寫的程式,是每個程式設計師必備的技能,也是幫助我們成長的非常重要的手段。

viewset的使用方式介紹

urls.py有變化哦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from django.urls import re_path

from viewsetter import views

urlpatterns = [
re_path(r'books/$', views.BookView.as_view({
'get': 'list',
'post': 'create'
})),
re_path(r'books/(?P<pk>\d+)/$', views.BookView.as_view({
'get': 'retrieve',
'put': 'update',
'delete': 'destroy'
})),
]

我們給as_view()方法傳遞了引數,這就是最神奇的地方。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# -*- coding: utf-8 -*-
# django rest framework元件
from rest_framework.viewsets import ModelViewSet

# 當前app中的模組
from .models import Book
from .serializer_classes import BookSerializer

# Create your views here.


class BookView(ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer

使用方式非常簡單,接下來,我們直接看原始碼。

viewset原始碼剖析
  • Django程式啟動,開始初始化,讀取urls.py, 讀取settings, 讀取檢視類
  • 執行as_views(), BookView沒有,需要到父類(ModelViewSet)中找
  • ModelViewSet繼承了mixins的幾個ModelMixin和GenericViewSet,顯然ModelMixin也沒有,只有GenericViewSet中有
  • GenericViewSet沒有任何程式碼,只繼承了ViewSetMixin和generics.GenericAPIView(這個我們已經認識了)
  • 繼續去ViewSetMixin中查詢,找到了as_view類方法,在重新封裝view函式的過程中,有一個self.action_map = actions
  • 這個actions就是我們給as_view()傳遞的引數
  • 繫結url和檢視函式(actions)之間的對映關係
  • 等待使用者請求
  • 接收到使用者請求,根據url找到檢視函式
  • 執行檢視函式的dispatch方法(因為檢視函式的返回值是:return self.dispatch()
  • dispatch分發請求,查詢到檢視類的五個方法中的某個
  • 開始執行,比如post請求,返回:self.create(),檢視類本身沒有,則會到父類中查詢
  • 最後在CreateModelMixin中查詢
  • 執行create()方法,獲取queryset和serializer_class
  • 返回資料

這就是viewset的優化方案,整個優化方案最重要的地方就是urls.py中我們傳入的引數,然後對引數進行對映關係繫結。

今天的全部內容到此就結束了。

今日總結

  • serializer進行put、delete及獲取單條資料的介面設計
  • 檢視元件的使用及原始碼剖析

                                    轉自:pizzali