白日依山尽,黄河入海流。欲穷千里目,更上一层楼。 -- 唐·王之涣

Django restframework实现批量操作

这篇文章主要介绍两种方式实现批量操作

一种是使用 Django restframework提供的装饰器action,可以更具实际情况扩展默认的增删改查操作,扩展性很好;另外一种是使用第三方模块 djangorestframework-bulk,这个模块简化了我们对于 对象本身增删改查的批量化操作,各有优缺点。实际工作中选择合适的就好。

本次分享中使用到的model模型

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

class User(models.Model):
name = models.CharField(max_length=32, unique=True, verbose_name='用户名')

def __str__(self):
return self.name

# Student 是后来添加的,`User` 在使用restframework-bulk 模块的时候存在问题
# 注意这里两个模型存在的差异性,你发现差在哪里了吗?
class Student(models.Model):
name = models.CharField(max_length=32)

def __str__(self):
return self.name

注意这里两个模型存在的差异性,你发现差在哪里了吗?


1、使用action装饰器自定义批量方法

为了分享方便,这里把serializers、viewset等都放到了一个文件(selfaction.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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from django.shortcuts import get_object_or_404
from app.models import Student, User

from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import status


# serializers
class UserModelSerializer(ModelSerializer):
class Meta:
model = User
fields = '__all__'


# viewset
class UserModelViewSet(ModelViewSet):
serializer_class = UserModelSerializer
queryset = User.objects.all()

# 通过 many=True 改造原有的API, 使其支持批量创建
# 核心就是如果过来的数据是list,则使用many=True
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)

# 使用action自定义方法
# detail 是针对记录是单条还是多条,这里是自定义批量操作,所以肯定是多条数据,detail=False
# 自定义批量删除, 方法名就是url的一部分,比如这里的 /v1/user/multi_delete/?ids=1,2,3
@action(methods=['delete'], detail=False)
def multi_delete(self, request, *args, **kwargs):
ids = request.query_params.get('ids', None)
if not ids:
return Response(status=status.HTTP_404_NOT_FOUND)

for id in ids.split(','):
get_object_or_404(User, id=int(id)).delete()
return Response(status=status.HTTP_204_NO_CONTENT)

# 自定义批量更新操作,
# 更新涉及到 全部 和 局部 更新,所以这里的methods要支持 put(全部)和 patch(局部)
@action(methods=['put', 'PATCH'], detail=False)
def multi_update(self, request, *args, **kwargs):
print("args: ", args, " kwargs: ", kwargs)
partial = kwargs.pop('partial', None)
print("partial: ", partial)
# 报错更新后的结果给前端
instances = []
if isinstance(self.request.data, list):
for item in request.data:
print(item)
instance = get_object_or_404(User, id=int(item['id']))
# partial 允许局部更新
serializer = super().get_serializer(instance, data=item, partial=partial)
serializer.is_valid(raise_exception=True)
serializer.save()
instances.append(serializer.data)
else:
pass
return Response(instances)

分析说明:

1、通过action装饰器可以实现自定义接口,可以扩展当前默认的增删改查操作
2、action的methods是该方法可执行的http method, detail参数是确定方法是针对单条数据还是多条数据
3、实际验证过发现partial = kwargs.pop('partial', None)不起作用,因为不管是PUT还是PATCH方法,该参数都是None,所以这里对于 partial的来源和用法有待进一步研究

1
2
args:  ()  kwargs:  {}
partial: None

4、使用DRF的Response返回的时候要求是JSON serializable,网上有些教程直接把get_object_or_404的结果instance追加到一个列表进行返回会报如下错误:

1
TypeError: Object of type User is not JSON serializable

所以追加到结果列表的时候使用 instances.append(serializer.data)

请求的接口验证

1、常规增删改查这里就赘述,可以参考文末整理的 《数据接口验证》
2、批量新增还是使用 /api/v1/users/接口,只不过body体是 List 格式,
3、批量更新(put/patch) 接口/api/v1/users/multi_update/
4、批量删除() 接口/api/v1/users/multi_delete/?ids=1,2,3,4这里的ids可以根据实际自定义


2、djangorestframework-bulk

以下配置可以放到一个文件或者放到几个独立的问题都可以

serializers.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# serializers.py
from rest_framework.serializers import ModelSerializer
from rest_framework.filters import SearchFilter
from rest_framework_bulk import BulkListSerializer, BulkSerializerMixin, BulkModelViewSet

from app.models import User
from app.models import Student

class UserSerializer(BulkSerializerMixin, ModelSerializer):
class Meta:
model = User
fields = '__all__'
list_serializer_class = BulkListSerializer
# filter_backends = (SearchFilter, )


class StudentSerializer(BulkSerializerMixin, ModelSerializer):
class Meta:
model = Student
fields = '__all__'
list_serializer_class = BulkListSerializer
filter_backends = (SearchFilter, )

filters.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
# fitlers.py
from django_filters.rest_framework.filterset import FilterSet
from django_filters import filters

from app.models import User
from app.models import Student


class UserFilterSet(FilterSet):
class Meta:
model = User
fields = {
'id': ['in'],
'name': ['icontains'],
}


class StudentFilterSet(FilterSet):
class Meta:
model = Student
fields = {
'id': ['in'],
'name': ['icontains'],
}

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
# views.py
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework_bulk import BulkModelViewSet

from app.models import User, Student
from app.serializers import UserSerializer, StudentSerializer
from app.filters import UserFilterSet, StudentFilterSet


# Create your views here.
class UserViewSet(BulkModelViewSet):
serializer_class = UserSerializer
queryset = User.objects.all()
print(queryset)
filter_backends = (DjangoFilterBackend, )
filter_class = UserFilterSet

def allow_bulk_destroy(self, qs, filtered):
print("here: qs-> ", qs, "\n filtered-> ", filtered)
return qs is not filtered


class StudentViewSet(BulkModelViewSet):
serializer_class = StudentSerializer
queryset = Student.objects.all()
filter_backends = (DjangoFilterBackend, )
filter_class = StudentFilterSet

def allow_bulk_destroy(self, qs, filtered):
print("here: qs-> ", qs, "\n filtered-> ", filtered)
return qs is not filtered

urls.py

1
2
3
4
5
6
7
8
9
10
11
12
from rest_framework_bulk.routes import BulkRouter
from app.views import UserViewSet
from app.views import StudentViewSet
from django.urls import path, include

router = BulkRouter()
router.register('users', UserViewSet)
router.register('students', StudentViewSet)

urlpatterns = [
path('', include(router.urls)),
]

数据接口验证

下表整理了常规接口和批量操作的接口汇总,大家可以按照这个实际进行测试

方法 api接口 参数 说明
GET http://127.0.0.1:8001/api/students/ 获取列表
GET http://127.0.0.1:8001/api/students/1/ 获取单个记录
GET http://127.0.0.1:8001/api/students/ ?id__in=2,3&name__icontains= 自定义过滤查询
POST http://127.0.0.1:8001/api/students/ { ‘name’: ‘Stu-01’} 新增单条记录
POST http://127.0.0.1:8001/api/students/ [{ ‘name’: ‘Stu-02’},{ ‘name’: ‘Stu-03’}] 批量新增
PUT/PATCH http://127.0.0.1:8001/api/students/ { ‘id’: 1, ‘name’: ‘Stu-01-ch’} 单个更新
PUT/PATCH http://127.0.0.1:8001/api/students/ [{ ‘id’: 2, ‘name’: ‘Stu-02-ch’},{ ‘id’: 3, ‘name’: ‘Stu-03-ch’}] 批量更新
DELETE http://127.0.0.1:8001/api/students/1/ 单个删除
DELETE http://127.0.0.1:8001/api/students/ ?id__in=2,3 批量删除

知识点说明:

1、使用 restframework-bulk批量删除时,是通过 allow_bulk_destroy这样的hook来判断要删除的结果是否是经过过滤的 qs is not filtered

1.1、所以必须定义过滤才可以,不然执行delete会不会删除

1
2
3
4
5
6
7
8
9
10
class StudentViewSet(BulkModelViewSet):
serializer_class = StudentSerializer
queryset = Student.objects.all()
# 注意注释这两个就不会删除,接口直接报 400 错误
# filter_backends = (DjangoFilterBackend, )
# filter_class = StudentFilterSet

def allow_bulk_destroy(self, qs, filtered):
print("here: qs-> ", qs, "\n filtered-> ", filtered)
return qs is not filtered

1.2、allow_bulk_destroy返回True 代表删除,返回 False 代表不删除,如果一旦没有上面的过滤限制,然后这个函数又直接返回True, 那就悲剧了…. 这个model的所有数据都被删除了…

1
2
3
4
5
6
7
8
9
10
11
12
class StudentViewSet(BulkModelViewSet):
serializer_class = StudentSerializer
queryset = Student.objects.all()
# 注意注释这两个就不会删除,接口直接报 400 错误
# filter_backends = (DjangoFilterBackend, )
# filter_class = StudentFilterSet

def allow_bulk_destroy(self, qs, filtered):
# print("here: qs-> ", qs, "\n filtered-> ", filtered)
# return qs is not filtered
# 直接返回True 是灾难的
return True

2、还记得刚开始的User模型么,除了批量更新之外所有的操作都是没有问题,为什么单独 批量更新就问题呢? 报错如下,提示没有pk属性,但是实际上该表是有 id作为主键的, id对应的就是 pk

QuerySet object has no attribute pk
QuerySet is null

明明和Student的代码一模一样。

一模一样吗? 当然存在差异,就是User模型中的name字段多了个 unique=True

去掉这个属性之后,再次执行批量更新操作没有任何问题。 但是最终未能找到问题的根源是什么? 大家有知道原因的,欢迎指导~

Refer:
1、https://zhuanlan.zhihu.com/p/369898862


感兴趣的可以扫码关注个人微信公众号,或者添加QQ 1209755822 备注技术交流

全栈运维

作者

Colin

发布于

2022-04-28

许可协议