Django restframework實現(xiàn)批量操作

WechatIMG582.jpeg

這篇文章主要介紹兩種方式實現(xiàn)批量操作

一種是使用 Django restframework提供的裝飾器action缩赛,可以更具實際情況擴(kuò)展默認(rèn)的增刪改查操作璧亮,擴(kuò)展性很好乖订;另外一種是使用第三方模塊 djangorestframework-bulk,這個模塊簡化了我們對于 對象本身增刪改查的批量化操作,各有優(yōu)缺點贞让。實際工作中選擇合適的就好周崭。

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

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 模塊的時候存在問題
# 注意這里兩個模型存在的差異性喳张,你發(fā)現(xiàn)差在哪里了嗎续镇?
class Student(models.Model):
    name = models.CharField(max_length=32)

    def __str__(self):
        return self.name

注意這里兩個模型存在的差異性,你發(fā)現(xiàn)差在哪里了嗎销部?


1摸航、使用action裝飾器自定義批量方法

為了分享方便,這里把serializers舅桩、viewset等都放到了一個文件(selfaction.py)中去

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, 使其支持批量創(chuàng)建
    # 核心就是如果過來的數(shù)據(jù)是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 是針對記錄是單條還是多條,這里是自定義批量操作擂涛,所以肯定是多條數(shù)據(jù)读串,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)
        # 報錯更新后的結(jié)果給前端
        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裝飾器可以實現(xiàn)自定義接口胀茵,可以擴(kuò)展當(dāng)前默認(rèn)的增刪改查操作
2、action的methods是該方法可執(zhí)行的http method挟阻, detail參數(shù)是確定方法是針對單條數(shù)據(jù)還是多條數(shù)據(jù)
3琼娘、實際驗證過發(fā)現(xiàn)partial = kwargs.pop('partial', None)不起作用,因為不管是PUT還是PATCH方法附鸽,該參數(shù)都是None脱拼,所以這里對于 partial的來源和用法有待進(jìn)一步研究

args:  ()  kwargs:  {}
partial:  None

4、使用DRF的Response返回的時候要求是JSON serializable坷备,網(wǎng)上有些教程直接把get_object_or_404的結(jié)果instance追加到一個列表進(jìn)行返回會報如下錯誤:

TypeError: Object of type User is not JSON serializable

所以追加到結(jié)果列表的時候使用 instances.append(serializer.data)

請求的接口驗證

1熄浓、常規(guī)增刪改查這里就贅述,可以參考文末整理的 《數(shù)據(jù)接口驗證》
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可以根據(jù)實際自定義


2、djangorestframework-bulk

以下配置可以放到一個文件或者放到幾個獨立的問題都可以

serializers.py

# 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

# 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

# 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

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

數(shù)據(jù)接口驗證

下表整理了常規(guī)接口和批量操作的接口匯總肥败,大家可以按照這個實際進(jìn)行測試

方法 api接口 參數(shù) 說明
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來判斷要刪除的結(jié)果是否是經(jīng)過過濾的 qs is not filtered

1.1皿哨、所以必須定義過濾才可以浅侨,不然執(zhí)行delete會不會刪除

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 代表不刪除,如果一旦沒有上面的過濾限制椎例,然后這個函數(shù)又直接返回True挨决, 那就悲劇了.... 這個model的所有數(shù)據(jù)都被刪除了...

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 是災(zāi)難的
        return True

2订歪、還記得剛開始的User模型么脖祈,除了批量更新之外所有的操作都是沒有問題,為什么單獨 批量更新就問題呢刷晋? 報錯如下盖高,提示沒有pk屬性,但是實際上該表是有id作為主鍵的, id對應(yīng)的就是 pk

djang-bulk-01.png
django-bulk-02.png

明明和Student的代碼一模一樣眼虱。

一模一樣嗎喻奥? 當(dāng)然存在差異,就是User模型中的name字段多了個 unique=True

去掉這個屬性之后捏悬,再次執(zhí)行批量更新操作沒有任何問題撞蚕。 但是最終未能找到問題的根源是什么? 大家有知道原因的过牙,歡迎指導(dǎo)~

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


感興趣的可以搜索微信公眾號 全棧運維,或者添加QQ 1209755822 備注技術(shù)交流

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寇钉,一起剝皮案震驚了整個濱河市刀疙,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扫倡,老刑警劉巖谦秧,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異撵溃,居然都是意外死亡疚鲤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門缘挑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來石咬,“玉大人,你說我怎么就攤上這事卖哎。” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵亏娜,是天一觀的道長焕窝。 經(jīng)常有香客問我,道長维贺,這世上最難降的妖魔是什么它掂? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮溯泣,結(jié)果婚禮上虐秋,老公的妹妹穿的比我還像新娘。我一直安慰自己垃沦,他們只是感情好客给,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肢簿,像睡著了一般靶剑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上池充,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天桩引,我揣著相機與錄音,去河邊找鬼收夸。 笑死坑匠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卧惜。 我是一名探鬼主播厘灼,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼序苏!你這毒婦竟也來了手幢?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤忱详,失蹤者是張志新(化名)和其女友劉穎围来,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匈睁,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡监透,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了航唆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胀蛮。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖糯钙,靈堂內(nèi)的尸體忽然破棺而出粪狼,到底是詐尸還是另有隱情退腥,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布再榄,位于F島的核電站狡刘,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏困鸥。R本人自食惡果不足惜嗅蔬,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疾就。 院中可真熱鬧澜术,春花似錦、人聲如沸猬腰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽漆诽。三九已至侮攀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厢拭,已是汗流浹背兰英。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留供鸠,地道東北人畦贸。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像楞捂,于是被迫代替她去往敵國和親薄坏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

推薦閱讀更多精彩內(nèi)容