深度解析Django REST Framework 批量操作

我们都知道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模型不是重点,只是为了丰富元素,展示一般场景。

创建数据:

  • 通过post方法访问127.0.0.1:8000/classrooms/创建一些教室数据。
  • 通过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:

  • url(r'foo/', ...)
  • 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

到此这篇关于深度解析Django REST Framework 批量操作的文章就介绍到这了,更多相关Django REST Framework批量操作内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • django rest framework之请求与响应(详解)

    前言:在上一篇文章,已经实现了访问指定URL就返回了指定的数据,这也体现了RESTful API的一个理念,每一个URL代表着一个资源.当然我们还知道RESTful API的另一个特性就是,发送不同的请求动作,会返还不同的响应,这篇文章就讲一下django-rest-framework这个工具在这方面给我们带来的便捷操作. 一.Request对象 平时我们在写Django的视图函数的时候,都会带上一个request参数,这样就能处理平时搭建网站时,浏览器访问网页时发出的常规的HttpReques

  • Django Rest framework权限的详细用法

    前言 我们都听过权限,那么权限到底是做什么的呢. 我们都有博客,或者去一些论坛,一定知道管理员这个角色, 比如我们申请博客的时候,一定要向管理员申请,也就是说管理员会有一些特殊的权利,是我们没有的. ==这些对某件事情决策的范围和程度,我们叫做权限==,权限是我们在项目开发中经常用到的. 本文将详细讲述DRF框架为我们提供的权限组件的使用方法. 源码剖析 DRF的版本控制.认证.权限.频率组件都在initial方法里初始化. 我们点进去看看: 其实我们版本.认证.权限.频率控制走的源码流程大致相

  • django rest framework 实现用户登录认证详解

    1.安装 pip install djangorestframework 2.创建项目及应用 创建项目 创建应用 目录结构如图 3.设置settings.py 设置数据库连接 # MySQL 增加mysql 连接 DATABASES = { 'default':{ 'ENGINE':'django.db.backends.mysql', 'HOST':'127.0.0.1', 'PORT':'3306', 'NAME':'dbname', # 数据库名 'USER':'username', 'P

  • django rest framework 数据的查找、过滤、排序的示例

    对于管理系统,常常需要展示列表数据,我们对于列表内的数据常常需要查找.过滤.排序等操作,其中查找等操作大部分是在后台进行的.django rest framework可以轻松的实现数据的查找.过滤等操作.接下来我们将以实际的例子进行介绍. 示例代码github地址: https://github.com/jinjidejuren/drf_learn 例如cmdb系统,作为资产管理系统常常需要对数据进行过滤或查找,获取期望的信息. 实现model 1.在这个示例项目中,需要实现对物理服务器的条件过

  • 详解Django rest_framework实现RESTful API

    一.什么是REST 面向资源是REST最明显的特征,资源是一种看待服务器的方式,将服务器看作是由很多离散的资源组成.每个资源是服务器上一个可命名的抽象概念.因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件.数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解. 与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词.一个资源可以由一个或多个URI来标识.URI既是资源的名称,也是资源在Web上的地址.对某个资

  • django-rest-framework解析请求参数过程详解

    前言 我们在django-rest-framework 自定义swagger 文章中编写了接口, 调通了接口文档. 接口文档可以直接填写参数进行请求, 接下来的问题是如何接受参数, 由于请求方式与参数序列化形式的不同, 接收参数的方式也有不同. 前提条件 服务端我们使用django-rest-framework编写接口. class ReturnJson(APIView): coreapi_fields=( DocParam("token"), ) def get(self, requ

  • Django rest framework实现分页的示例

    第一种分页PageNumberPagination 基本使用 (1)urls.py urlpatterns = [ re_path('(?P<version>[v1|v2]+)/page1/', Pager1View.as_view(),) #分页1 ] (2)api/utils/serializers/pager.py # api/utils/serializsers/pager.py from rest_framework import serializers from api impor

  • Django rest framework基本介绍与代码示例

    本文研究的主要是Django rest framework的相关内容,分享了example,具体如下. Django REST框架是构建Web API的强大而灵活的工具包. 您可能希望使用REST框架的一些原因: Web浏览的API是您的开发人员的巨大的可用性胜利. 验证策略包括OAuth1a和OAuth2的包. 支持ORM和非ORM数据源的序列化. 如果不需要功能更强大的功能,可以自定义一切 - 只需使用基于功能的常规视图. 广泛的文档和极好的社区支持. 由Mozilla,Red Hat,He

  • Django REST framework 视图和路由详解

    DRF中的Request 在Django REST Framework中内置的Request类扩展了Django中的Request类,实现了很多方便的功能--如请求数据解析和认证等. 比如,区别于Django中的request从request.GET中获取URL参数,从request.POST中取某些情况下的POST数据. 在APIView中封装的request,就实现了请求数据的解析: 对于GET请求的参数我们通过request.query_params来获取. 对于POST请求.PUT请求的

  • 深度解析Django REST Framework 批量操作

    我们都知道Django rest framework这个库,默认只支持批量查看,不支持批量更新(局部或整体)和批量删除. 下面我们来讨论这个问题,看看如何实现批量更新和删除操作. DRF基本情况 我们以下面的代码作为例子: models: from django.db import models # Create your models here. class Classroom(models.Model): location = models.CharField(max_length=128)

  • Django REST framework 单元测试实例解析

    这篇文章主要介绍了Django REST framework 单元测试实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 环境 Win10 Python3.7 Django2.2 项目 参照官网 快速开始 写了一个 demo 测试 参照官网 测试 和 Django 的测试差不多 创建 tutorial/tests/tests.py import json from django.test import TestCase from rest_

  • Django Rest Framework实现身份认证源码详解

    目录 一.Django框架 二.身份认证的两种实现方式: 三.身份认证源码解析流程 一.Django框架 Django确实是一个很强大,用起来很爽的一个框架,在Rest Framework中已经将身份认证全都封装好了,用的时候直接导入authentication.py这个模块就好了.这个模块中5个认证类.但是我们在开发中很少用自带的认证类,而是根据项目实际需要去自己实现认证类.下面是内置的认证类 BaseAuthentication(object):所有的认证相关的类都继承自这个类,我们写的认证

  • 深度解析MySQL启动时报“The server quit without updating PID file”错误的原因

    很多童鞋在启动mysql的时候,碰到过这个错误, 首先,澄清一点,出现这个错误的前提是:通过服务脚本来启动mysql.通过mysqld_safe或mysqld启动mysql实例并不会报这个错误. 那么,出现这个错误的原因具体是什么呢? 哈哈,对分析过程不care的童鞋可直接跳到文末的总结部分~ 总结 下面,来分析下mysql的服务启动脚本 脚本完整内容如下: #!/bin/sh # Copyright Abandoned 1996 TCX DataKonsult AB & Monty Progr

  • JavaScript中 this 指向问题深度解析

    JavaScript 中的 this 指向问题有很多文章在解释,仍然有很多人问.上周我们的开发团队连续两个人遇到相关问题,所以我不得不将关于前端构建技术的交流会延长了半个时候讨论 this 的问题. 与我们常见的很多语言不同,JavaScript 函数中的 this 指向并不是在函数定义的时候确定的,而是在调用的时候确定的.换句话说, 函数的调用方式决定了 this 指向 . JavaScript 中,普通的函数调用方式有三种:直接调用.方法调用和 new 调用.除此之外,还有一些特殊的调用方式

  • 浅谈Django REST Framework限速

    官方文档 settings.py配置 REST_FRAMEWORK = { 'DEFAULT_THROTTLE_CLASSES': ( 'rest_framework.throttling.AnonRateThrottle', 'rest_framework.throttling.UserRateThrottle' ), 'DEFAULT_THROTTLE_RATES': { 'anon': '100/day', 'user': '1000/day' } } AnonRateThrottle:用

随机推荐