深度解析Django REST Framework 批量操作

大江東流發表於2021-05-05

我們都知道Django rest framework這個庫,預設只支援批量檢視,不支援批量更新(區域性或整體)和批量刪除。

下面我們來討論這個問題,看看如何實現批量更新和刪除操作。

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')),
]

這是一個相當簡單而又經典的場景。其中的Classroom模型不是重點,只是為了豐富元素,展示一般場景。

建立資料:

  1. 通過post方法訪問127.0.0.1:8000/classrooms/建立一些教室資料。
  2. 通過post方法訪問127.0.0.1:8000/students/建立一些學生資料。



可以很清楚地看到DRF預設:

  • 通過GET /students/檢視所有的學生
  • 通過GET /students/1/檢視id為1的學生
  • 通過POST /students/攜帶一個資料字典,建立單個學生
  • 通過PUT/students/1/整體更新id為1的學生資訊
  • 通過PATCH /students/1/區域性更新id為1的學生資訊
  • 通過DELETE/students/1/刪除id為1的學生

沒有批量更新和刪除的介面。

並且當我們嘗試向/students/,POST一個攜帶了多個資料字典的列表物件時,比如下面的資料:

[
    {
        "name": "alex",
        "classroom": 1
    },
    {
        "name": "mary",
        "classroom": 2
    },
    {
        "name": "kk",
        "classroom": 3
    }
]

反饋給我們的如下圖所示:

錯誤提示:非法的資料,期望一個字典,但你提供了一個列表。

至於嘗試向更新和刪除介面提供多個物件的id,同樣無法操作。

可見在DRF中,預設情況下,只能批量檢視,不能批量建立、修改和刪除。

自定義批量操作

現實中,難免有批量的建立、修改和刪除需求。那怎麼辦呢?只能自己寫程式碼實現了。

下面是初學者隨便寫的程式碼,未考慮資料合法性、安全性、可擴充套件性等等,僅僅是最基礎的實現了功能而已:

批量建立

class StudentViewSet(ModelViewSet):
    serializer_class = StudentSerializer
    queryset = Student.objects.all()

    # 通過many=True直接改造原有的API,使其可以批量建立
    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本身提供了一個ListSerializer,這個類是實現批量建立的核心關鍵。

當我們在例項化一個序列化器的時候,有一個關鍵字引數many,如果將它設定為True,就表示我們要進行批量操作,DRF在後臺會自動使用ListSerializer來替代預設的Serializer。

所以,實現批量建立的核心就是如何將many引數新增進去。

這裡,我們重寫了get_serializer方法,通過if isinstance(self.request.data, list):語句,分析前端傳送過來的資料到底是個字典還是個列表。如果是個字典,表示這是建立單個物件,如果是個列表,表示是建立批量物件。

讓我們測試一下。首先,依然可以正常地建立單個物件。

然後如下面的方式,通過POST 往/students/傳送一個列表:

這裡有個坑,可能會碰到AttributeError: 'ListSerializer' object has no attribute 'fields'錯誤。

這是響應資料格式的問題。沒關係。重新整理頁面即可。

也可以在POSTMAN中進行測試,就不會出現這個問題。

批量刪除

先上程式碼:

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,使其可以批量建立
    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)

    # 新增一個批量刪除的API。刪除單個物件,依然建議使用原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)

要注意,原DRF是通過DELETE/students/1/刪除id為1的學生。

那麼如果我想批量刪除id為1,3,5的三個資料怎麼辦?

反正肯定是不能往/students/1/這樣的url傳送請求的。

那麼是構造一條這樣的url嗎?/students/1,3,5/?或者/students/?pk=1,3,5

還是往/students/傳送json資料[1,3,5]?

這裡,我採用/students/multiple_delete/?pks=1,3,5的形式。

這樣,它建立了一條新的介面,既避開了/students/這個介面,也能通過url傳送引數。


由於我們的檢視繼承的是ModelViewSet,所以需要通過action裝飾器,增加一個同名的multiple_delete()方法。

為了防止id和Python內建的id函式衝突。我們這裡使用pks作為url的引數名。

通過一個for迴圈,分割逗號獲取批量主鍵值。

通過主鍵值去資料庫中查詢物件,然後刪除。(這裡只是實現功能,未處理異常)

下面,最好在POSTMAN中測試一下:

注意請求是DELETE /students/multiple_delete/?pks=4,5

再訪問/students/,可以看到相關資料確實被刪除了。

批量更新

程式碼如下:

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,使其可以批量建立
    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)

    # 新增一個批量刪除的API。刪除單個物件,依然建議使用原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)

    # 新增一個批量修改的API。更新單個物件,依然建議使用原API
    # 通過PUT方法訪問url domain.com/students/multiple_update/
    # 傳送json格式的資料,資料是個列表,列表中的每一項是個字典,每個字典是一個例項
    @action(methods=['put'], detail=False)
    def multiple_update(self, request, *args, **kwargs):
        partial = kwargs.pop('partial', False)
        instances = []  # 這個變數是用於儲存修改過後的物件,返回給前端
        for item in request.data:  # 遍歷列表中的每個物件字典
            instance = get_object_or_404(Student, id=int(item['id']))  # 通過ORM查詢例項
            # 構造序列化物件,注意partial=True表示允許區域性更新
            # 由於我們前面重寫了get_serializer方法,進行了many=True的判斷。
            # 但此處不需要many=True的判斷,所以必須呼叫父類的get_serializer方法
            serializer = super().get_serializer(instance, data=item, partial=partial)
            serializer.is_valid(raise_exception=True)
            serializer.save()
            instances.append(serializer.data)  # 將資料新增到列表中
        return Response(instances)

更新和刪除不同的地方在於,它在提供主鍵值的同時,還需要提供新的欄位值。

所以,這裡我們將主鍵值放在json資料中,而不是作為url的引數。

請仔細閱讀上面的程式碼註釋。

這裡有個小技巧,其實可以根據HTTP的PUT和PATCH的不同,靈活設定partial引數的值。

另外,要注意的對get_serializer()方法的處理。

下面測試一下。在POSTMAN中通過PUT方法,訪問/students/multiple_update/,並攜帶如下的json資料:

[
    {
        "id":2,
    	"name":"tom",
    	"classroom":3
    },
    {
        "id":3,
        "name":"jack",
        "classroom":2
    }
]

上面是整體更新,區域性更新也是可以的。

djangorestframework-bulk

前面,我們通過蹩腳的程式碼,實現了最基礎的批量增刪改查。

但問題太多,不夠優雅清晰、異常未處理、邊界未考慮等等,實在是太爛。

事實上,有這麼個djangorestframework-bulk庫,已經高水平地實現了我們的需求。

這個庫非常簡單,核心的其實只有3個模組,核心程式碼也就300行左右,非常短小精幹,建議精讀它的原始碼,肯定會有收穫。

官網:https://pypi.org/project/djangorestframework-bulk/

github:https://github.com/miki725/django-rest-framework-bulk

最後更新:2015年4月

最後版本:0.2.1

它有兩個序列化器的版本:drf2\drf3。我們用drf3。

依賴

  • Python > = 2.7

  • 的Django > = 1.3

  • Django REST framework > = 3.0.0

安裝

使用pip:

$ pip install djangorestframework-bulk

範例

檢視

我們註釋掉前面章節中的程式碼,編寫下面的程式碼,使用bulk庫來實現批量操作。

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

# 序列化器。暫時寫在檢視模組裡
# 必須先繼承BulkSerializerMixin,由它將只讀欄位id的值寫回到validated_data中,才能實現更新操作。
class StudentSerializer(BulkSerializerMixin, ModelSerializer):
    class Meta(object):
        model = Student
        fields = '__all__'
        
        # 在Meta類下面的list_serializer_class選項用來修改當`many=True`時使用的類。
        # 預設情況下,DRF使用的是ListSerializer。
        # 但是ListSerializer沒有實現自己的批量update方法。
        # 在DRF3中如果需要批量更新物件,則需定義此屬性,並編寫ListSerializer的子類
        # 所以bulk庫提供了一個BulkListSerializer類
        # 它直接繼承了ListSerializer,並重寫了update方法。
        list_serializer_class = BulkListSerializer
        
        # 這條可以不寫。但實際上,批量刪除需要搭配過濾操作
        filter_backends = (SearchFilter,) 

# 檢視集
class StudentView(BulkModelViewSet):
    queryset = Student.objects.all()
    serializer_class = StudentSerializer

    def allow_bulk_destroy(self, qs, filtered):
        # 這裡作為例子,簡單粗暴地直接允許批量刪除
        return True

然後我們將自動獲得下面的功能:

# 批量查詢
GET    http://127.0.0.1/students/


# 建立單個物件
POST  	http://127.0.0.1/students/
body   {"field":"value","field2":"value2"}    傳送字典格式的json資料


# 建立多個物件
POST 	http://127.0.0.1/students/
body	[{"field":"value","field2":"value2"}]   傳送列表格式的json資料


# 更新多個物件(需要提供所有欄位的值)
PUT 	http://127.0.0.1/students/
body	[{"field":"value","field2":"value2"}]   傳送列表格式的json資料


# 區域性更新多個物件(不需要提供所有欄位的值)
PATCH 	http://127.0.0.1/students/
body 	[{"field":"value"}]                     傳送列表格式的json資料


# 刪除多個物件
DELETE   http://127.0.0.1/students/

當然,原生的單個物件的操作也是依然支援的!

要特別注意DELETE操作,這個例子裡會直接將所有的資料全部刪除。如果你想刪除指定的一批資料,可以搭配filter_backends來過濾查詢集,使用allow_bulk_destroy方法來自定義刪除策略。

可以看到bulk庫對於RESTful的url沒有任何改動,非常優雅,比我們上面的蹩腳方法強太多。

路由

路由也需要修改一下。

bulk的路由可以自動對映批量操作,它對DRF原生的DefaultRouter進行了簡單的封裝:

from rest_framework_bulk.routes import BulkRouter
from .views import StudentView

router = BulkRouter()
router.register(r'students', StudentView)

urlpatterns = router.urls

測試

現在可以測試一下。下面提供一部分測試資料:

[
    {
        "name": "s1",
        "classroom": 1
    },
    {
        "name": "s2",
        "classroom": 3
    },
    {
        "name": "s3",
        "classroom": 2
    }
]
  • 建議在POSTMAN中進行測試
  • PUT和PATCH要攜帶id值
  • PUT要攜帶所有欄位的值
  • PATCH可以只攜帶要更新的欄位的值
  • DELETE一定要小心

可以看到功能完全實現,批量操作成功。

DRF3相關

DRF3的API相比DRF2具有很多變化,尤其是在序列化器上。要在DRF3上使用bulk,需要注意以下幾點:

  • 如果你的檢視需要批量更新功能,則必須指定 list_serializer_class (也就是繼承了 BulkUpdateModelMixin時)

  • DRF3 從 serializer.validated_data中移除了只讀欄位。所以,無法關聯 validated_dataListSerializer ,因為缺少模型主鍵這個只讀欄位。為了解決這個問題,你必須在你的序列化類中使用 BulkSerializerMixin ,這個混入類會新增模型主鍵欄位到 validated_data中。預設情況,模型主鍵是 id ,你可以通過 update_lookup_field 屬性來指定主鍵名:

    class FooSerializer(BulkSerializerMixin, ModelSerializer):
        class Meta(object):
            model = FooModel
            list_serializer_class = BulkListSerializer
            update_lookup_field = 'slug'
    

注意事項

大多數API的每種資源都有兩個級別的url:

  1. url(r'foo/', ...)
  2. url(r'foo/(?P<pk>\d+)/', ...)

但是,第二個URL不適用於批量操作,因為該URL直接對映到單個資源。因此,所有批量通用檢視僅適用於第一個URL。

如果只需要某個單獨的批量操作功能,bulk提供了多個通用檢視類。例如,ListBulkCreateAPIView 將僅執行批量建立操作。有關可用的通用檢視類的完整列表,請訪問generics.py的原始碼。

大多數批量操作都是安全的,因為資料都是和每個物件關聯的。例如,如果您需要更新3個特定資源,則必須在PUTPATCH的請求資料中明確的標識出那些資源的id。唯一的例外是批量刪除,例如對第一種URL的DELETE請求可能會刪除所有資源,而無需任何特殊確認。為了解決這個問題,批量刪除混入類中提供了一個鉤子,以確定是否應允許執行該批量刪除請求,也就是allow_bulk_destroy方法:

class FooView(BulkDestroyAPIView):
    def allow_bulk_destroy(self, qs, filtered):
        # 你的自定義業務邏輯寫在這裡

        # qs引數是一個查詢集,它來自self.get_queryset()
        # 預設要檢查qs是否被過濾了。
        # filtered引數來自self.filter_queryset(qs)
        return qs is not filtered   # 最終返回True,則執行刪除操作。返回False,則不執行。

預設情況下,allow_bulk_destroy方法會檢查查詢集是否已過濾,如果沒有過濾,則不允許執行該批量刪除操作。此處的邏輯是,你知道自己在刪除哪些物件,知道自己沒有進行全部物件的刪除操作。通俗地說就是,程式設計師對你的程式碼在作什麼,心裡要有數。

原始碼解讀

下圖是目錄組織結構。分drf2和drf3,基本使用drf3。test目錄我們不關心。

核心其實就是根目錄下的5個模組和drf3目錄。其中的models.py檔案是空的,沒有程式碼。

__init__.py

這個模組就是簡單地匯入其它模組:

__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規範檢測工具,這個地方不需要檢測。

也可以在一個檔案的第一行增加 #flake8:NOQA 來告訴規範檢測工具,這個檔案不用檢查。

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

就是針對不同的DRF版本,匯入不同的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類似,針對不同的DRF版本,匯入不同的mixins。

routes.py

搭配bulk的BulkModelViewSet檢視類進行工作。

原始碼:

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中的處理函式
    """
    routes = copy.deepcopy(SimpleRouter.routes)
    routes[0].mapping.update({
        'put': 'bulk_update',
        'patch': 'partial_bulk_update',
        'delete': 'bulk_destroy',
    })

對DRF原生的DefaultRouter路由模組進行再次封裝,主要是修改三個HTTP方法的對映關係,將它們對映到bulk庫的mixins方法。

generics.py

這個模組的風格和DRF的原始碼非常類似,都是各種繼承搭配出來各種類檢視。

裡面混用了DRF原生的mixin和bulk自己寫的mixin。

主要是將http的method對映到檢視類中對應的處理方法。

原始碼:

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
# ################################################## #

# 批量建立
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)

# 批量檢視和建立
# 注意批量檢視依然使用的是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)

# 批量檢視、單個建立、批量更新
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)

# 這個功能最全面
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)


# ########################################################## #
# 專門提供的一個viewset,搭配了批量建立、更新和刪除功能
# 它需要搭配bulk的router模組使用。
# 如果不用這個,就用ListBulkCreateUpdateDestroyAPIView
# ########################################################## #

class BulkModelViewSet(bulk_mixins.BulkCreateModelMixin,
                       bulk_mixins.BulkUpdateModelMixin,
                       bulk_mixins.BulkDestroyModelMixin,
                       ModelViewSet):
    pass

drf3/mixins.py

這個模組實現了核心的業務邏輯。請注意閱讀原始碼中的註釋。

原始碼:

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.以後的版本多了一個many=True的引數。
    通過這個引數,可以實現單個和批量建立例項的統一操作。
    其本質是使用DRF提供的ListSerializer類
    """
	# 重寫create方法
    def create(self, request, *args, **kwargs):
        # 通過判斷request.data變數是列表還是字典,來區分是單體操作還是批量操作。
        # 這要求我們前端傳送json格式的資料時,必須定義好資料格式
        bulk = isinstance(request.data, list)

        if not bulk: # 如果不是批量操作,則呼叫父類的單體建立方法
            return super(BulkCreateModelMixin, self).create(request, *args, **kwargs)

        else:  # 如果是批量操作,則新增many=True引數
            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)
	# 這是個鉤子方法
    def perform_bulk_create(self, serializer):
        return self.perform_create(serializer)


class BulkUpdateModelMixin(object):
    """
	同樣是通過many=True引數來實現批量更新
    """
	# 重寫單個物件的獲取
    def get_object(self):
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
		# 這個if執行的是父類的操作
        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/的區別
        return
	
    # 核心的更新方法
    def bulk_update(self, request, *args, **kwargs):
        # 先看看是PUT還是PATCH
        partial = kwargs.pop('partial', False)

        # 限制只對過濾後的查詢集進行更新
        # 下面的程式碼就是基本的DRF反序列化套路
        # 核心是instances是個過濾集,many指定為True,partial根據方法來變
        # 這裡的邏輯是將單體更新當作只有一個元素的列表來更新(也就是批量為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方法,則手動新增partial=True引數,表示區域性更新
    # 實際執行的方法和整體更新一樣,都是呼叫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):
    """
    用於刪除模型例項
    """

    def allow_bulk_destroy(self, qs, filtered):
        """
        這是一個鉤子,用於確保批量刪除操作是安全的。
        預設情況下,它會檢查刪除操作是否在一個過濾集上進行,不能對原始查詢集也就是qs進行刪除。
		最終的返回值是布林值,如果返回True,表示允許刪除,否則拒絕。
		原始碼這裡是簡單地比較了qs和filtered是否相同,你可以自定義判斷邏輯。
		刪除操作可以配合過濾後端。
        """
        return qs is not filtered
	# DELETE方法將被轉發到這裡
    def bulk_destroy(self, request, *args, **kwargs):
        # 首先,獲取查詢集
        qs = self.get_queryset()
		# 獲取過濾集
        filtered = self.filter_queryset(qs)
        # 呼叫allow_bulk_destroy方法,判斷是否允許該刪除操作
        if not self.allow_bulk_destroy(qs, filtered):
            # 如果不允許,返回400響應,錯誤的請求
            return Response(status=status.HTTP_400_BAD_REQUEST)
		# 否則對過濾集執行批量刪除操作
        self.perform_bulk_destroy(filtered)

        return Response(status=status.HTTP_204_NO_CONTENT)
	
    # 這個刪除方法,其實就是ORM的delete方法
    # 之所以設定這個方法,其實就是個鉤子,方便我們自定義
    def perform_destroy(self, instance):
        instance.delete()
	
    # 批量刪除很簡單,就是遍歷過濾集,逐個刪除
    def perform_bulk_destroy(self, objects):
        for obj in objects:
            self.perform_destroy(obj)

drf3/serializers.py

這個模組只有兩個類,它們提供了2個功能。

  • BulkSerializerMixin:往驗證後的資料中新增主鍵欄位的值
  • BulkListSerializer:提供批量更新的update方法

原始碼:

from __future__ import print_function, unicode_literals
import inspect  # Python內建模組。從活動的Python物件獲取有用的資訊。

from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ListSerializer


__all__ = [
    'BulkListSerializer',
    'BulkSerializerMixin',
]

# 由於DRF原始碼在預設情況下,會將只讀欄位的值去掉,所以id主鍵值不會出現在validated_data中
# 因為我們現在需要批量更新物件,url中也沒有攜帶物件的id,所以我們需要手動將id的值新增回去。
class BulkSerializerMixin(object):
    # 由外部資料轉換為Python內部字典
    def to_internal_value(self, data):
        # 先呼叫父類的方法,獲得返回值
        ret = super(BulkSerializerMixin, self).to_internal_value(data)
		# 去Meta元類中看看,有沒有指定'update_lookup_field'屬性,如果沒有,預設使用id
        # 這本質就是個鉤子,允許我們自定義主鍵欄位
        id_attr = getattr(self.Meta, 'update_lookup_field', 'id')
        # 獲取當前請求的型別
        request_method = getattr(getattr(self.context.get('view'), 'request'), 'method', '')
		
        # 如果下面的三個條件都滿足:
        # self.root是BulkListSerializer的例項
        # id_attr變數不為空
        # 請求的方法是'PUT'或'PATCH'
        # 那麼執行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追加鍵值對
            ret[id_attr] = id_value

        return ret

# 這個類主要是在ListSerializer基礎上重寫的update邏輯,實現批量操作
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去獲取所有的鍵值對
        # 下面是一個字典推導式
        all_validated_data_by_id = {
            i.pop(id_attr): i
            for i in all_validated_data
        }
		# 對資料型別做判斷
        if not all((bool(i) and not inspect.isclass(i)
                    for i in all_validated_data_by_id.keys())):
            raise ValidationError('')


        # 使用ORM從查詢集中過濾出那些需要更新的模型例項
        # 比如id__in=[1,3,4]
        objects_to_update = queryset.filter(**{
            '{}__in'.format(id_attr): all_validated_data_by_id.keys(),
        })
		# 如果過濾出來的模型例項數量和用於更新的資料數量不一致,彈出異常
        if len(all_validated_data_by_id) != objects_to_update.count():
            raise ValidationError('Could not find all objects to update.')
		# 準備一個空列表,用於儲存將要被更新的例項
        updated_objects = []
		# 迴圈每個例項
        for obj in objects_to_update:
            obj_id = getattr(obj, id_attr)
            obj_validated_data = all_validated_data_by_id.get(obj_id)
            # 使用模型序列化器的update方法進行實際的更新動作,以防update方法在別的地方被覆蓋
            updated_objects.append(self.child.update(obj, obj_validated_data))

        return updated_objects

相關文章