深度解析Django REST Framework 批量操作
我們都知道Django rest framework這個(gè)庫,默認(rèn)只支持批量查看,不支持批量更新(局部或整體)和批量刪除。
下面我們來討論這個(gè)問題,看看如何實(shí)現(xiàn)批量更新和刪除操作。
DRF基本情況
我們以下面的代碼作為例子:
models:
from django.db import models # Create your models here. class Classroom(models.Model): location = models.CharField(max_length=128) def __str__(self): return self.location class Student(models.Model): name = models.CharField(max_length=32) classroom = models.ForeignKey(Classroom, on_delete=models.CASCADE) def __str__(self): return self.name
serializers:
from .models import Classroom, Student from rest_framework.serializers import ModelSerializer class StudentSerializer(ModelSerializer): class Meta: model = Student fields = "__all__" class ClassroomSerializer(ModelSerializer): class Meta: model = Classroom fields = "__all__"
views:
from rest_framework.viewsets import ModelViewSet from .serializers import StudentSerializer, ClassroomSerializer from .models import Student, Classroom class StudentViewSet(ModelViewSet): serializer_class = StudentSerializer queryset = Student.objects.all() class ClassroomViewSet(ModelViewSet): serializer_class = ClassroomSerializer queryset = Classroom.objects.all()
myapp/urls:
from rest_framework.routers import DefaultRouter from .views import StudentViewSet, ClassroomViewSet router = DefaultRouter() router.register(r'students', StudentViewSet) router.register(r'classrooms', ClassroomViewSet) urlpatterns = router.urls
根urls:
from django.contrib import admin from django.urls import path,include urlpatterns = [ path('admin/', admin.site.urls), path('', include('myapp.urls')), ]
這是一個(gè)相當(dāng)簡單而又經(jīng)典的場(chǎng)景。其中的Classroom模型不是重點(diǎn),只是為了豐富元素,展示一般場(chǎng)景。
創(chuàng)建數(shù)據(jù):
- 通過post方法訪問127.0.0.1:8000/classrooms/創(chuàng)建一些教室數(shù)據(jù)。
- 通過post方法訪問127.0.0.1:8000/students/創(chuàng)建一些學(xué)生數(shù)據(jù)。
可以很清楚地看到DRF默認(rèn):
- 通過GET
/students/
查看所有的學(xué)生 - 通過GET
/students/1/
查看id為1的學(xué)生 - 通過POST
/students/
攜帶一個(gè)數(shù)據(jù)字典,創(chuàng)建單個(gè)學(xué)生 - 通過PUT
/students/1/
整體更新id為1的學(xué)生信息 - 通過PATCH
/students/1/
局部更新id為1的學(xué)生信息 - 通過DELETE
/students/1/
刪除id為1的學(xué)生
沒有批量更新和刪除的接口。
并且當(dāng)我們嘗試向/students/
,POST一個(gè)攜帶了多個(gè)數(shù)據(jù)字典的列表對(duì)象時(shí),比如下面的數(shù)據(jù):
[ { "name": "alex", "classroom": 1 }, { "name": "mary", "classroom": 2 }, { "name": "kk", "classroom": 3 } ]
反饋給我們的如下圖所示:
錯(cuò)誤提示:非法的數(shù)據(jù),期望一個(gè)字典,但你提供了一個(gè)列表。
至于嘗試向更新和刪除接口提供多個(gè)對(duì)象的id,同樣無法操作。
可見在DRF中,默認(rèn)情況下,只能批量查看,不能批量創(chuàng)建、修改和刪除。
自定義批量操作
現(xiàn)實(shí)中,難免有批量的創(chuàng)建、修改和刪除需求。那怎么辦呢?只能自己寫代碼實(shí)現(xiàn)了。
下面是初學(xué)者隨便寫的代碼,未考慮數(shù)據(jù)合法性、安全性、可擴(kuò)展性等等,僅僅是最基礎(chǔ)的實(shí)現(xiàn)了功能而已:
批量創(chuàng)建
class StudentViewSet(ModelViewSet): serializer_class = StudentSerializer queryset = Student.objects.all() # 通過many=True直接改造原有的API,使其可以批量創(chuàng)建 def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() kwargs.setdefault('context', self.get_serializer_context()) if isinstance(self.request.data, list): return serializer_class(many=True, *args, **kwargs) else: return serializer_class(*args, **kwargs)
DRF本身提供了一個(gè)ListSerializer,這個(gè)類是實(shí)現(xiàn)批量創(chuàng)建的核心關(guān)鍵。
當(dāng)我們?cè)趯?shí)例化一個(gè)序列化器的時(shí)候,有一個(gè)關(guān)鍵字參數(shù)many,如果將它設(shè)置為True,就表示我們要進(jìn)行批量操作,DRF在后臺(tái)會(huì)自動(dòng)使用ListSerializer來替代默認(rèn)的Serializer。
所以,實(shí)現(xiàn)批量創(chuàng)建的核心就是如何將many參數(shù)添加進(jìn)去。
這里,我們重寫了get_serializer方法,通過if isinstance(self.request.data, list):
語句,分析前端發(fā)送過來的數(shù)據(jù)到底是個(gè)字典還是個(gè)列表。如果是個(gè)字典,表示這是創(chuàng)建單個(gè)對(duì)象,如果是個(gè)列表,表示是創(chuàng)建批量對(duì)象。
讓我們測(cè)試一下。首先,依然可以正常地創(chuàng)建單個(gè)對(duì)象。
然后如下面的方式,通過POST 往/students/
發(fā)送一個(gè)列表:
這里有個(gè)坑,可能會(huì)碰到AttributeError: 'ListSerializer' object has no attribute 'fields'錯(cuò)誤。
這是響應(yīng)數(shù)據(jù)格式的問題。沒關(guān)系。刷新頁面即可。
也可以在POSTMAN中進(jìn)行測(cè)試,就不會(huì)出現(xiàn)這個(gè)問題。
批量刪除
先上代碼:
from rest_framework.viewsets import ModelViewSet from .serializers import StudentSerializer, ClassroomSerializer from .models import Student, Classroom from rest_framework import status from rest_framework.response import Response from django.shortcuts import get_object_or_404 from rest_framework.decorators import action class StudentViewSet(ModelViewSet): serializer_class = StudentSerializer queryset = Student.objects.all() # 通過many=True直接改造原有的API,使其可以批量創(chuàng)建 def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() kwargs.setdefault('context', self.get_serializer_context()) if isinstance(self.request.data, list): return serializer_class(many=True, *args, **kwargs) else: return serializer_class(*args, **kwargs) # 新增一個(gè)批量刪除的API。刪除單個(gè)對(duì)象,依然建議使用原API # 通過DELETE訪問訪問url domain.com/students/multiple_delete/?pks=4,5 @action(methods=['delete'], detail=False) def multiple_delete(self, request, *args, **kwargs): # 獲取要?jiǎng)h除的對(duì)象們的主鍵值 pks = request.query_params.get('pks', None) if not pks: return Response(status=status.HTTP_404_NOT_FOUND) for pk in pks.split(','): get_object_or_404(Student, id=int(pk)).delete() return Response(status=status.HTTP_204_NO_CONTENT)
要注意,原DRF是通過DELETE/students/1/
刪除id為1的學(xué)生。
那么如果我想批量刪除id為1,3,5的三個(gè)數(shù)據(jù)怎么辦?
反正肯定是不能往/students/1/
這樣的url發(fā)送請(qǐng)求的。
那么是構(gòu)造一條這樣的url嗎?/students/1,3,5/
?或者/students/?pk=1,3,5
還是往/students/
發(fā)送json數(shù)據(jù)[1,3,5]
?
這里,我采用/students/multiple_delete/?pks=1,3,5
的形式。
這樣,它創(chuàng)建了一條新的接口,既避開了/students/
這個(gè)接口,也能通過url發(fā)送參數(shù)。
由于我們的視圖繼承的是ModelViewSet,所以需要通過action裝飾器,增加一個(gè)同名的multiple_delete()方法。
為了防止id和Python內(nèi)置的id函數(shù)沖突。我們這里使用pks作為url的參數(shù)名。
通過一個(gè)for循環(huán),分割逗號(hào)獲取批量主鍵值。
通過主鍵值去數(shù)據(jù)庫中查找對(duì)象,然后刪除。(這里只是實(shí)現(xiàn)功能,未處理異常)
下面,最好在POSTMAN中測(cè)試一下:
注意請(qǐng)求是DELETE /students/multiple_delete/?pks=4,5
再訪問/students/
,可以看到相關(guān)數(shù)據(jù)確實(shí)被刪除了。
批量更新
代碼如下:
from rest_framework.viewsets import ModelViewSet from .serializers import StudentSerializer, ClassroomSerializer from .models import Student, Classroom from rest_framework import status from rest_framework.response import Response from django.shortcuts import get_object_or_404 from rest_framework.decorators import action class StudentViewSet(ModelViewSet): serializer_class = StudentSerializer queryset = Student.objects.all() # 通過many=True直接改造原有的API,使其可以批量創(chuàng)建 def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() kwargs.setdefault('context', self.get_serializer_context()) if isinstance(self.request.data, list): return serializer_class(many=True, *args, **kwargs) else: return serializer_class(*args, **kwargs) # 新增一個(gè)批量刪除的API。刪除單個(gè)對(duì)象,依然建議使用原API # 通過DELETE訪問訪問url domain.com/students/multiple_delete/?pks=4,5 @action(methods=['delete'], detail=False) def multiple_delete(self, request, *args, **kwargs): pks = request.query_params.get('pks', None) if not pks: return Response(status=status.HTTP_404_NOT_FOUND) for pk in pks.split(','): get_object_or_404(Student, id=int(pk)).delete() return Response(status=status.HTTP_204_NO_CONTENT) # 新增一個(gè)批量修改的API。更新單個(gè)對(duì)象,依然建議使用原API # 通過PUT方法訪問url domain.com/students/multiple_update/ # 發(fā)送json格式的數(shù)據(jù),數(shù)據(jù)是個(gè)列表,列表中的每一項(xiàng)是個(gè)字典,每個(gè)字典是一個(gè)實(shí)例 @action(methods=['put'], detail=False) def multiple_update(self, request, *args, **kwargs): partial = kwargs.pop('partial', False) instances = [] # 這個(gè)變量是用于保存修改過后的對(duì)象,返回給前端 for item in request.data: # 遍歷列表中的每個(gè)對(duì)象字典 instance = get_object_or_404(Student, id=int(item['id'])) # 通過ORM查找實(shí)例 # 構(gòu)造序列化對(duì)象,注意partial=True表示允許局部更新 # 由于我們前面重寫了get_serializer方法,進(jìn)行了many=True的判斷。 # 但此處不需要many=True的判斷,所以必須調(diào)用父類的get_serializer方法 serializer = super().get_serializer(instance, data=item, partial=partial) serializer.is_valid(raise_exception=True) serializer.save() instances.append(serializer.data) # 將數(shù)據(jù)添加到列表中 return Response(instances)
更新和刪除不同的地方在于,它在提供主鍵值的同時(shí),還需要提供新的字段值。
所以,這里我們將主鍵值放在json數(shù)據(jù)中,而不是作為url的參數(shù)。
請(qǐng)仔細(xì)閱讀上面的代碼注釋。
這里有個(gè)小技巧,其實(shí)可以根據(jù)HTTP的PUT和PATCH的不同,靈活設(shè)定partial參數(shù)的值。
另外,要注意的對(duì)get_serializer()方法的處理。
下面測(cè)試一下。在POSTMAN中通過PUT方法,訪問/students/multiple_update/
,并攜帶如下的json數(shù)據(jù):
[ { "id":2, "name":"tom", "classroom":3 }, { "id":3, "name":"jack", "classroom":2 } ]
上面是整體更新,局部更新也是可以的。
djangorestframework-bulk
前面,我們通過蹩腳的代碼,實(shí)現(xiàn)了最基礎(chǔ)的批量增刪改查。
但問題太多,不夠優(yōu)雅清晰、異常未處理、邊界未考慮等等,實(shí)在是太爛。
事實(shí)上,有這么個(gè)djangorestframework-bulk庫,已經(jīng)高水平地實(shí)現(xiàn)了我們的需求。
這個(gè)庫非常簡單,核心的其實(shí)只有3個(gè)模塊,核心代碼也就300行左右,非常短小精干,建議精讀它的源碼,肯定會(huì)有收獲。
官網(wǎng):https://pypi.org/project/djangorestframework-bulk/
github:https://github.com/miki725/django-rest-framework-bulk
最后更新:2015年4月
最后版本:0.2.1
它有兩個(gè)序列化器的版本:drf2\drf3。我們用drf3。
依賴
- Python > = 2.7
- 的Django > = 1.3
- Django REST framework > = 3.0.0
安裝
使用pip:
$ pip install djangorestframework-bulk
范例
視圖
我們注釋掉前面章節(jié)中的代碼,編寫下面的代碼,使用bulk庫來實(shí)現(xiàn)批量操作。
bulk中的views(和mixins)非常類似drf原生的generic views(和mixins)
from rest_framework.serializers import ModelSerializer from .models import Student from rest_framework_bulk import ( BulkListSerializer, BulkSerializerMixin, BulkModelViewSet ) from rest_framework.filters import SearchFilter # 序列化器。暫時(shí)寫在視圖模塊里 # 必須先繼承BulkSerializerMixin,由它將只讀字段id的值寫回到validated_data中,才能實(shí)現(xiàn)更新操作。 class StudentSerializer(BulkSerializerMixin, ModelSerializer): class Meta(object): model = Student fields = '__all__' # 在Meta類下面的list_serializer_class選項(xiàng)用來修改當(dāng)`many=True`時(shí)使用的類。 # 默認(rèn)情況下,DRF使用的是ListSerializer。 # 但是ListSerializer沒有實(shí)現(xiàn)自己的批量update方法。 # 在DRF3中如果需要批量更新對(duì)象,則需定義此屬性,并編寫ListSerializer的子類 # 所以bulk庫提供了一個(gè)BulkListSerializer類 # 它直接繼承了ListSerializer,并重寫了update方法。 list_serializer_class = BulkListSerializer # 這條可以不寫。但實(shí)際上,批量刪除需要搭配過濾操作 filter_backends = (SearchFilter,) # 視圖集 class StudentView(BulkModelViewSet): queryset = Student.objects.all() serializer_class = StudentSerializer def allow_bulk_destroy(self, qs, filtered): # 這里作為例子,簡單粗暴地直接允許批量刪除 return True
然后我們將自動(dòng)獲得下面的功能:
# 批量查詢 GET http://127.0.0.1/students/ # 創(chuàng)建單個(gè)對(duì)象 POST http://127.0.0.1/students/ body {"field":"value","field2":"value2"} 發(fā)送字典格式的json數(shù)據(jù) # 創(chuàng)建多個(gè)對(duì)象 POST http://127.0.0.1/students/ body [{"field":"value","field2":"value2"}] 發(fā)送列表格式的json數(shù)據(jù) # 更新多個(gè)對(duì)象(需要提供所有字段的值) PUT http://127.0.0.1/students/ body [{"field":"value","field2":"value2"}] 發(fā)送列表格式的json數(shù)據(jù) # 局部更新多個(gè)對(duì)象(不需要提供所有字段的值) PATCH http://127.0.0.1/students/ body [{"field":"value"}] 發(fā)送列表格式的json數(shù)據(jù) # 刪除多個(gè)對(duì)象 DELETE http://127.0.0.1/students/
當(dāng)然,原生的單個(gè)對(duì)象的操作也是依然支持的!
要特別注意DELETE操作,這個(gè)例子里會(huì)直接將所有的數(shù)據(jù)全部刪除。如果你想刪除指定的一批數(shù)據(jù),可以搭配filter_backends
來過濾查詢集,使用allow_bulk_destroy
方法來自定義刪除策略。
可以看到bulk庫對(duì)于RESTful的url沒有任何改動(dòng),非常優(yōu)雅,比我們上面的蹩腳方法強(qiáng)太多。
路由
路由也需要修改一下。
bulk的路由可以自動(dòng)映射批量操作,它對(duì)DRF原生的DefaultRouter進(jìn)行了簡單的封裝:
from rest_framework_bulk.routes import BulkRouter from .views import StudentView router = BulkRouter() router.register(r'students', StudentView) urlpatterns = router.urls
測(cè)試
現(xiàn)在可以測(cè)試一下。下面提供一部分測(cè)試數(shù)據(jù):
[ { "name": "s1", "classroom": 1 }, { "name": "s2", "classroom": 3 }, { "name": "s3", "classroom": 2 } ]
- 建議在POSTMAN中進(jìn)行測(cè)試
- PUT和PATCH要攜帶id值
- PUT要攜帶所有字段的值
- PATCH可以只攜帶要更新的字段的值
- DELETE一定要小心
可以看到功能完全實(shí)現(xiàn),批量操作成功。
DRF3相關(guān)
DRF3的API相比DRF2具有很多變化,尤其是在序列化器上。要在DRF3上使用bulk,需要注意以下幾點(diǎn):
如果你的視圖需要批量更新功能,則必須指定 list_serializer_class
(也就是繼承了 BulkUpdateModelMixin
時(shí))
DRF3 從 serializer.validated_data
中移除了只讀字段。所以,無法關(guān)聯(lián) validated_data
和ListSerializer
,因?yàn)槿鄙倌P椭麈I這個(gè)只讀字段。為了解決這個(gè)問題,你必須在你的序列化類中使用 BulkSerializerMixin
,這個(gè)混入類會(huì)添加模型主鍵字段到 validated_data
中。默認(rèn)情況,模型主鍵是 id
,你可以通過 update_lookup_field
屬性來指定主鍵名:
class FooSerializer(BulkSerializerMixin, ModelSerializer): class Meta(object): model = FooModel list_serializer_class = BulkListSerializer update_lookup_field = 'slug'
注意事項(xiàng)
大多數(shù)API的每種資源都有兩個(gè)級(jí)別的url:
url(r'foo/', ...)
url(r'foo/(?P<pk>\d+)/', ...)
但是,第二個(gè)URL不適用于批量操作,因?yàn)樵揢RL直接映射到單個(gè)資源。因此,所有批量通用視圖僅適用于第一個(gè)URL。
如果只需要某個(gè)單獨(dú)的批量操作功能,bulk提供了多個(gè)通用視圖類。例如,ListBulkCreateAPIView
將僅執(zhí)行批量創(chuàng)建操作。有關(guān)可用的通用視圖類的完整列表,請(qǐng)?jiān)L問generics.py
的源代碼。
大多數(shù)批量操作都是安全的,因?yàn)閿?shù)據(jù)都是和每個(gè)對(duì)象關(guān)聯(lián)的。例如,如果您需要更新3個(gè)特定資源,則必須在PUT
或PATCH
的請(qǐng)求數(shù)據(jù)中明確的標(biāo)識(shí)出那些資源的id。唯一的例外是批量刪除,例如對(duì)第一種URL的DELETE
請(qǐng)求可能會(huì)刪除所有資源,而無需任何特殊確認(rèn)。為了解決這個(gè)問題,批量刪除混入類中提供了一個(gè)鉤子,以確定是否應(yīng)允許執(zhí)行該批量刪除請(qǐng)求,也就是allow_bulk_destroy
方法:
class FooView(BulkDestroyAPIView): def allow_bulk_destroy(self, qs, filtered): # 你的自定義業(yè)務(wù)邏輯寫在這里 # qs參數(shù)是一個(gè)查詢集,它來自self.get_queryset() # 默認(rèn)要檢查qs是否被過濾了。 # filtered參數(shù)來自self.filter_queryset(qs) return qs is not filtered # 最終返回True,則執(zhí)行刪除操作。返回False,則不執(zhí)行。
默認(rèn)情況下,allow_bulk_destroy
方法會(huì)檢查查詢集是否已過濾,如果沒有過濾,則不允許執(zhí)行該批量刪除操作。此處的邏輯是,你知道自己在刪除哪些對(duì)象,知道自己沒有進(jìn)行全部對(duì)象的刪除操作。通俗地說就是,程序員對(duì)你的代碼在作什么,心里要有數(shù)。
源碼解讀
下圖是目錄組織結(jié)構(gòu)。分drf2和drf3,基本使用drf3。test目錄我們不關(guān)心。
核心其實(shí)就是根目錄下的5個(gè)模塊和drf3目錄。其中的models.py文件是空的,沒有代碼。
__init__.py
這個(gè)模塊就是簡單地導(dǎo)入其它模塊:
__version__ = '0.2.1' __author__ = 'Miroslav Shubernetskiy' try: from .generics import * # noqa from .mixins import * # noqa from .serializers import * # noqa except Exception: pass
#NOQA 注釋的作用是告訴PEP8規(guī)范檢測(cè)工具,這個(gè)地方不需要檢測(cè)。
也可以在一個(gè)文件的第一行增加 #flake8:NOQA 來告訴規(guī)范檢測(cè)工具,這個(gè)文件不用檢查。
serializers.py
源代碼:
# 這是用于Python版本兼容,print方法和Unicode字符 from __future__ import print_function, unicode_literals import rest_framework if str(rest_framework.__version__).startswith('2'): from .drf2.serializers import * # noqa else: from .drf3.serializers import * # noqa
就是針對(duì)不同的DRF版本,導(dǎo)入不同的serializers。
mixins.py
源代碼:
from __future__ import print_function, unicode_literals import rest_framework if str(rest_framework.__version__).startswith('2'): from .drf2.mixins import * # noqa else: from .drf3.mixins import * # noqa
和serializers.py類似,針對(duì)不同的DRF版本,導(dǎo)入不同的mixins。
routes.py
搭配bulk的BulkModelViewSet視圖類進(jìn)行工作。
源代碼:
from __future__ import unicode_literals, print_function import copy from rest_framework.routers import DefaultRouter, SimpleRouter __all__ = [ 'BulkRouter', ] class BulkRouter(DefaultRouter): """ 將http的method映射到bulk的minxins中的處理函數(shù) """ routes = copy.deepcopy(SimpleRouter.routes) routes[0].mapping.update({ 'put': 'bulk_update', 'patch': 'partial_bulk_update', 'delete': 'bulk_destroy', })
對(duì)DRF原生的DefaultRouter路由模塊進(jìn)行再次封裝,主要是修改三個(gè)HTTP方法的映射關(guān)系,將它們映射到bulk庫的mixins方法。
generics.py
這個(gè)模塊的風(fēng)格和DRF的源碼非常類似,都是各種繼承搭配出來各種類視圖。
里面混用了DRF原生的mixin和bulk自己寫的mixin。
主要是將http的method映射到視圖類中對(duì)應(yīng)的處理方法。
源代碼:
from __future__ import unicode_literals, print_function from rest_framework import mixins from rest_framework.generics import GenericAPIView from rest_framework.viewsets import ModelViewSet from . import mixins as bulk_mixins __all__ = [ 'BulkCreateAPIView', 'BulkDestroyAPIView', 'BulkModelViewSet', 'BulkUpdateAPIView', 'ListBulkCreateAPIView', 'ListBulkCreateDestroyAPIView', 'ListBulkCreateUpdateAPIView', 'ListBulkCreateUpdateDestroyAPIView', 'ListCreateBulkUpdateAPIView', 'ListCreateBulkUpdateDestroyAPIView', ] # ################################################## # # 下面是一些具體的視圖類。通過將mixin類與基視圖組合來提供方法處理程序。 # 基本前面繼承一堆mixins,后面繼承GenericAPIView # ################################################## # # 批量創(chuàng)建 class BulkCreateAPIView(bulk_mixins.BulkCreateModelMixin, GenericAPIView): def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) # 批量更新(局部和整體) class BulkUpdateAPIView(bulk_mixins.BulkUpdateModelMixin, GenericAPIView): def put(self, request, *args, **kwargs): return self.bulk_update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.partial_bulk_update(request, *args, **kwargs) # 批量刪除 class BulkDestroyAPIView(bulk_mixins.BulkDestroyModelMixin, GenericAPIView): def delete(self, request, *args, **kwargs): return self.bulk_destroy(request, *args, **kwargs) # 批量查看和創(chuàng)建 # 注意批量查看依然使用的是DRF原生的ListModelMixin提供的功能 class ListBulkCreateAPIView(mixins.ListModelMixin, bulk_mixins.BulkCreateModelMixin, GenericAPIView): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) # 批量查看、單個(gè)創(chuàng)建、批量更新 class ListCreateBulkUpdateAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, bulk_mixins.BulkUpdateModelMixin, GenericAPIView): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.bulk_update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.partial_bulk_update(request, *args, **kwargs) class ListCreateBulkUpdateDestroyAPIView(mixins.ListModelMixin, mixins.CreateModelMixin, bulk_mixins.BulkUpdateModelMixin, bulk_mixins.BulkDestroyModelMixin, GenericAPIView): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.bulk_update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.partial_bulk_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.bulk_destroy(request, *args, **kwargs) class ListBulkCreateUpdateAPIView(mixins.ListModelMixin, bulk_mixins.BulkCreateModelMixin, bulk_mixins.BulkUpdateModelMixin, GenericAPIView): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.bulk_update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.partial_bulk_update(request, *args, **kwargs) class ListBulkCreateDestroyAPIView(mixins.ListModelMixin, bulk_mixins.BulkCreateModelMixin, bulk_mixins.BulkDestroyModelMixin, GenericAPIView): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.bulk_destroy(request, *args, **kwargs) # 這個(gè)功能最全面 class ListBulkCreateUpdateDestroyAPIView(mixins.ListModelMixin, bulk_mixins.BulkCreateModelMixin, bulk_mixins.BulkUpdateModelMixin, bulk_mixins.BulkDestroyModelMixin, GenericAPIView): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def put(self, request, *args, **kwargs): return self.bulk_update(request, *args, **kwargs) def patch(self, request, *args, **kwargs): return self.partial_bulk_update(request, *args, **kwargs) def delete(self, request, *args, **kwargs): return self.bulk_destroy(request, *args, **kwargs) # ########################################################## # # 專門提供的一個(gè)viewset,搭配了批量創(chuàng)建、更新和刪除功能 # 它需要搭配bulk的router模塊使用。 # 如果不用這個(gè),就用ListBulkCreateUpdateDestroyAPIView # ########################################################## # class BulkModelViewSet(bulk_mixins.BulkCreateModelMixin, bulk_mixins.BulkUpdateModelMixin, bulk_mixins.BulkDestroyModelMixin, ModelViewSet): pass
drf3/mixins.py
這個(gè)模塊實(shí)現(xiàn)了核心的業(yè)務(wù)邏輯。請(qǐng)注意閱讀源代碼中的注釋。
源代碼:
from __future__ import print_function, unicode_literals from rest_framework import status from rest_framework.mixins import CreateModelMixin from rest_framework.response import Response __all__ = [ 'BulkCreateModelMixin', 'BulkDestroyModelMixin', 'BulkUpdateModelMixin', ] class BulkCreateModelMixin(CreateModelMixin): """ Django REST >= 2.2.5.以后的版本多了一個(gè)many=True的參數(shù)。 通過這個(gè)參數(shù),可以實(shí)現(xiàn)單個(gè)和批量創(chuàng)建實(shí)例的統(tǒng)一操作。 其本質(zhì)是使用DRF提供的ListSerializer類 """ # 重寫create方法 def create(self, request, *args, **kwargs): # 通過判斷request.data變量是列表還是字典,來區(qū)分是單體操作還是批量操作。 # 這要求我們前端發(fā)送json格式的數(shù)據(jù)時(shí),必須定義好數(shù)據(jù)格式 bulk = isinstance(request.data, list) if not bulk: # 如果不是批量操作,則調(diào)用父類的單體創(chuàng)建方法 return super(BulkCreateModelMixin, self).create(request, *args, **kwargs) else: # 如果是批量操作,則添加many=True參數(shù) serializer = self.get_serializer(data=request.data, many=True) serializer.is_valid(raise_exception=True) # 這里少了DRF源碼中的headers = self.get_success_headers(serializer.data) self.perform_bulk_create(serializer) return Response(serializer.data, status=status.HTTP_201_CREATED) # 這是個(gè)鉤子方法 def perform_bulk_create(self, serializer): return self.perform_create(serializer) class BulkUpdateModelMixin(object): """ 同樣是通過many=True參數(shù)來實(shí)現(xiàn)批量更新 """ # 重寫單個(gè)對(duì)象的獲取 def get_object(self): lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field # 這個(gè)if執(zhí)行的是父類的操作 if lookup_url_kwarg in self.kwargs: return super(BulkUpdateModelMixin, self).get_object() # 如果沒有攜帶id,則直接返回,什么都不做。 # 也就是 PUT http://127.0.0.1/students/ # 和 PUT http://127.0.0.1/students/1/的區(qū)別 return # 核心的更新方法 def bulk_update(self, request, *args, **kwargs): # 先看看是PUT還是PATCH partial = kwargs.pop('partial', False) # 限制只對(duì)過濾后的查詢集進(jìn)行更新 # 下面的代碼就是基本的DRF反序列化套路 # 核心是instances是個(gè)過濾集,many指定為True,partial根據(jù)方法來變 # 這里的邏輯是將單體更新當(dāng)作只有一個(gè)元素的列表來更新(也就是批量為1)。 serializer = self.get_serializer( self.filter_queryset(self.get_queryset()), data=request.data, many=True, partial=partial, ) serializer.is_valid(raise_exception=True) self.perform_bulk_update(serializer) return Response(serializer.data, status=status.HTTP_200_OK) # 如果是PATCH方法,則手動(dòng)添加partial=True參數(shù),表示局部更新 # 實(shí)際執(zhí)行的方法和整體更新一樣,都是調(diào)用bulk_update方法 def partial_bulk_update(self, request, *args, **kwargs): kwargs['partial'] = True return self.bulk_update(request, *args, **kwargs) # 鉤子方法 def perform_update(self, serializer): serializer.save() # 鉤子方法 def perform_bulk_update(self, serializer): return self.perform_update(serializer) # 刪除操作 class BulkDestroyModelMixin(object): """ 用于刪除模型實(shí)例 """ def allow_bulk_destroy(self, qs, filtered): """ 這是一個(gè)鉤子,用于確保批量刪除操作是安全的。 默認(rèn)情況下,它會(huì)檢查刪除操作是否在一個(gè)過濾集上進(jìn)行,不能對(duì)原始查詢集也就是qs進(jìn)行刪除。 最終的返回值是布爾值,如果返回True,表示允許刪除,否則拒絕。 源碼這里是簡單地比較了qs和filtered是否相同,你可以自定義判斷邏輯。 刪除操作可以配合過濾后端。 """ return qs is not filtered # DELETE方法將被轉(zhuǎn)發(fā)到這里 def bulk_destroy(self, request, *args, **kwargs): # 首先,獲取查詢集 qs = self.get_queryset() # 獲取過濾集 filtered = self.filter_queryset(qs) # 調(diào)用allow_bulk_destroy方法,判斷是否允許該刪除操作 if not self.allow_bulk_destroy(qs, filtered): # 如果不允許,返回400響應(yīng),錯(cuò)誤的請(qǐng)求 return Response(status=status.HTTP_400_BAD_REQUEST) # 否則對(duì)過濾集執(zhí)行批量刪除操作 self.perform_bulk_destroy(filtered) return Response(status=status.HTTP_204_NO_CONTENT) # 這個(gè)刪除方法,其實(shí)就是ORM的delete方法 # 之所以設(shè)置這個(gè)方法,其實(shí)就是個(gè)鉤子,方便我們自定義 def perform_destroy(self, instance): instance.delete() # 批量刪除很簡單,就是遍歷過濾集,逐個(gè)刪除 def perform_bulk_destroy(self, objects): for obj in objects: self.perform_destroy(obj)
drf3/serializers.py
這個(gè)模塊只有兩個(gè)類,它們提供了2個(gè)功能。
- BulkSerializerMixin:往驗(yàn)證后的數(shù)據(jù)中添加主鍵字段的值
- BulkListSerializer:提供批量更新的update方法
源代碼:
from __future__ import print_function, unicode_literals import inspect # Python內(nèi)置模塊。從活動(dòng)的Python對(duì)象獲取有用的信息。 from rest_framework.exceptions import ValidationError from rest_framework.serializers import ListSerializer __all__ = [ 'BulkListSerializer', 'BulkSerializerMixin', ] # 由于DRF源碼在默認(rèn)情況下,會(huì)將只讀字段的值去掉,所以id主鍵值不會(huì)出現(xiàn)在validated_data中 # 因?yàn)槲覀儸F(xiàn)在需要批量更新對(duì)象,url中也沒有攜帶對(duì)象的id,所以我們需要手動(dòng)將id的值添加回去。 class BulkSerializerMixin(object): # 由外部數(shù)據(jù)轉(zhuǎn)換為Python內(nèi)部字典 def to_internal_value(self, data): # 先調(diào)用父類的方法,獲得返回值 ret = super(BulkSerializerMixin, self).to_internal_value(data) # 去Meta元類中看看,有沒有指定'update_lookup_field'屬性,如果沒有,默認(rèn)使用id # 這本質(zhì)就是個(gè)鉤子,允許我們自定義主鍵字段 id_attr = getattr(self.Meta, 'update_lookup_field', 'id') # 獲取當(dāng)前請(qǐng)求的類型 request_method = getattr(getattr(self.context.get('view'), 'request'), 'method', '') # 如果下面的三個(gè)條件都滿足: # self.root是BulkListSerializer的實(shí)例 # id_attr變量不為空 # 請(qǐng)求的方法是'PUT'或'PATCH' # 那么執(zhí)行if語句中的代碼 if all((isinstance(self.root, BulkListSerializer), id_attr, request_method in ('PUT', 'PATCH'))): # 拿到id字段的句柄 id_field = self.fields[id_attr] # 拿到字段的值 id_value = id_field.get_value(data) # 為ret追加鍵值對(duì) ret[id_attr] = id_value return ret # 這個(gè)類主要是在ListSerializer基礎(chǔ)上重寫的update邏輯,實(shí)現(xiàn)批量操作 class BulkListSerializer(ListSerializer): # 指定用于更新的查詢字段為id update_lookup_field = 'id' def update(self, queryset, all_validated_data): # 先看看有沒有指定用于查詢的字段 id_attr = getattr(self.child.Meta, 'update_lookup_field', 'id') # 通過id去獲取所有的鍵值對(duì) # 下面是一個(gè)字典推導(dǎo)式 all_validated_data_by_id = { i.pop(id_attr): i for i in all_validated_data } # 對(duì)數(shù)據(jù)類型做判斷 if not all((bool(i) and not inspect.isclass(i) for i in all_validated_data_by_id.keys())): raise ValidationError('') # 使用ORM從查詢集中過濾出那些需要更新的模型實(shí)例 # 比如id__in=[1,3,4] objects_to_update = queryset.filter(**{ '{}__in'.format(id_attr): all_validated_data_by_id.keys(), }) # 如果過濾出來的模型實(shí)例數(shù)量和用于更新的數(shù)據(jù)數(shù)量不一致,彈出異常 if len(all_validated_data_by_id) != objects_to_update.count(): raise ValidationError('Could not find all objects to update.') # 準(zhǔn)備一個(gè)空列表,用于保存將要被更新的實(shí)例 updated_objects = [] # 循環(huán)每個(gè)實(shí)例 for obj in objects_to_update: obj_id = getattr(obj, id_attr) obj_validated_data = all_validated_data_by_id.get(obj_id) # 使用模型序列化器的update方法進(jìn)行實(shí)際的更新動(dòng)作,以防update方法在別的地方被覆蓋 updated_objects.append(self.child.update(obj, obj_validated_data)) return updated_objects
到此這篇關(guān)于深度解析Django REST Framework 批量操作的文章就介紹到這了,更多相關(guān)Django REST Framework批量操作內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python pandas自定義函數(shù)的使用方法示例
這篇文章主要介紹了Python pandas自定義函數(shù)的使用方法,結(jié)合實(shí)例形式分析了pandas模塊相關(guān)自定義函數(shù)數(shù)值運(yùn)算操作技巧,需要的朋友可以參考下2019-11-11python代碼實(shí)現(xiàn)圖書管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了python代碼實(shí)現(xiàn)圖書管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11Python遠(yuǎn)程SSH庫Paramiko詳細(xì)操作
paramiko實(shí)現(xiàn)了SSHv2協(xié)議(底層使用cryptography),用于連接遠(yuǎn)程服務(wù)器并執(zhí)行相關(guān)操作,使用該模塊可以對(duì)遠(yuǎn)程服務(wù)器進(jìn)行命令或文件操作,今天通過本文給大家介紹Python遠(yuǎn)程SSH庫Paramiko簡介,感興趣的朋友一起看看吧2022-05-05python實(shí)現(xiàn)漢諾塔遞歸算法經(jīng)典案例
這篇文章主要大家分享了python實(shí)現(xiàn)漢諾塔遞歸算法經(jīng)典案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05Python3.0與2.X版本的區(qū)別實(shí)例分析
這篇文章主要介紹了Python3.0與2.X版本的區(qū)別,包含了一些常見的區(qū)別及分析,還有筆者的一些感悟,需要的朋友可以參考下2014-08-08用PyQt進(jìn)行Python圖形界面的程序的開發(fā)的入門指引
這篇文章主要介紹了用PyQt進(jìn)行Python圖形界面的程序的開發(fā)的入門指引,來自于IBM官方網(wǎng)站技術(shù)文檔,需要的朋友可以參考下2015-04-04探究數(shù)組排序提升Python程序的循環(huán)的運(yùn)行效率的原因
這篇文章主要介紹了探究數(shù)組排序提升Python程序的循環(huán)的運(yùn)行效率的原因,作者用代碼實(shí)踐了多個(gè)小片段來進(jìn)行對(duì)比解釋,需要的朋友可以參考下2015-04-04python整小時(shí) 整天時(shí)間戳獲取算法示例
今天小編就為大家分享一篇python整小時(shí) 整天時(shí)間戳獲取算法示例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2019-02-02