django-rest-framework:在 ModelViewSet 中添加批量操作

django-rest-framework: Adding bulk operation in a ModelViewSet

我有许多端点使用 ModelViewSet 来管理我的模型的 CRUD 操作。

我想做的是在这些相同的端点添加批量创建、更新和删除。换句话说,我想将 POSTPUTPATCHDELETE 添加到收集端点(例如:/api/v1/my-model)。有一个可用的 django-rest-framework-bulk 包,但它似乎已被废弃(4 年未更新),我不习惯在生产中使用不再活跃的包。

此外,这里有几个类似的问题有解决方案,以及我找到的博客文章。但是,它们似乎都使用基础 ViewSetAPIView,这将需要重写我现有的所有 ModelViewSet 代码。

最后,可以选择使用 @action 装饰器,但这需要我有一个单独的列表端点(例如- /api/v1/my-model/bulk),我想避免这一点。

有没有其他方法可以在保持现有 ModelViewSet 视图的同时完成此操作?我一直在研究 GenericViewSet 和 mixins,想知道创建我自己的 mixin 是否可行。但是,查看 mixin 代码,您似乎无法指定要附加到给定 mixin 的 HTTP 请求方法。

最后,我尝试创建一个单独的 ViewSet 接受 PUT 并将其添加到我的 URL,但这不起作用(当我尝试 PUT 到 /api/v1/my-model).我试过的代码如下所示:

# views.py
class MyModelViewSet(viewsets.ModelViewSet):
    serializer_class = MyModelSerializer
    permission_classes = (IsAuthenticated,)
    queryset = MyModel.objects.all()
    paginator = None

class ListMyModelView(viewsets.ViewSet):
    permission_classes = (IsAuthenticated,)

    def put(self, request):
        # Code for updating list of models will go here.

        return Response({'test': 'list put!'})


# urls.py
router = DefaultRouter(trailing_slash=False)
router.register(r'my-model', MyModelViewSet)
router.register(r'my-model', ListMyModelView, base_name='list-my-model')

urlpatterns = [
    path('api/v1/', include(router.urls)),
    # more paths for auth, admin, etc..
]

想法?

我知道你说过你想避免添加额外的操作,但我认为这是批量更新现有视图的最简单方法 create/update/delete。

您可以创建一个 mixin,添加到您的视图中来处理所有事情,您只需更改现有视图和序列化程序中的一行即可。

假设您的 ListSerializer 看起来与 DRF documentation 相似,mixin 将如下所示。

core/serializers.py

class BulkUpdateSerializerMixin:
    """
    Mixin to be used with BulkUpdateListSerializer & BulkUpdateRouteMixin
    that adds the ID back to the internal value from the raw input data so
    that it's included in the validated data.
    """
    def passes_test(self):
        # Must be an update method for the ID to be added to validated data
        test = self.context['request'].method in ('PUT', 'PATCH')
        test &= self.context.get('bulk_update', False)
        return test

    def to_internal_value(self, data):
        ret = super().to_internal_value(data)

        if self.passes_test():
            ret['id'] = self.fields['id'].get_value(data)

        return ret

core/views.py

class BulkUpdateRouteMixin:
    """
    Mixin that adds a `bulk_update` API route to a view set. To be used
    with BulkUpdateSerializerMixin & BulkUpdateListSerializer.
    """
    def get_object(self):
        # Override to return None if the lookup_url_kwargs is not present.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
        if lookup_url_kwarg in self.kwargs:
            return super().get_object()
        return

    def get_serializer(self, *args, **kwargs):
        # Initialize serializer with `many=True` if the data passed
        # to the serializer is a list.
        if self.request.method in ('PUT', 'PATCH'):
            data = kwargs.get('data', None)
            kwargs['many'] = isinstance(data, list)
        return super().get_serializer(*args, **kwargs)

    def get_serializer_context(self):
        # Add `bulk_update` flag to the serializer context so that
        # the id field can be added back to the validated data through
        # `to_internal_value()`
        context = super().get_serializer_context()
        if self.action == 'bulk_update':
            context['bulk_update'] = True
        return context

    @action(detail=False, methods=['put'], url_name='bulk_update')
    def bulk_update(self, request, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(
            queryset,
            data=request.data,
            many=True,
        )
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
        return Response(serializer.data, status=status.HTTP_200_OK)

然后你就可以继承mixins

class MyModelSerializer(BulkUpdateSerializerMixin
                        serializers.ModelSerializer):
    class Meta:
        model = MyModel
        list_serializer_class = BulkUpdateListSerializer

class MyModelViewSet(BulkUpdateRouteMixin,
                     viewsets.ModelViewSet):
    ...

你的 PUT 请求只需要指向 '/api/v1/my-model/bulk_update'

更新的 mixin 不需要额外的视图集操作:

对于批量操作,向列表视图提交一个 POST 请求,并将数据作为列表。

class BulkUpdateSerializerMixin:
    def passes_test(self):
        test = self.context['request'].method in ('POST',)
        test &= self.context.get('bulk', False)
        return test

    def to_internal_value(self, data):
        ret = super().to_internal_value(data)

        if self.passes_test():
            ret['id'] = self.fields['id'].get_value(data)

        return ret

get_serializer() 中有一个检查以确保只有 POST 请求可以被接受以进行批量操作。如果它是 POST 并且请求数据是一个列表,则添加一个标志,以便可以将 ID 字段添加回经过验证的数据,并且您的 ListSerializer 可以处理批量操作。

class BulkUpdateViewSetMixin:
    def get_serializer(self, *args, **kwargs):
        serializer_class = self.get_serializer_class()
        kwargs['context'] = self.get_serializer_context()
        if self.request.method in ('POST',):
            data = kwargs.get('data', None)
            is_bulk = isinstance(data, list)
            kwargs['many'] = is_bulk
            kwargs['context']['bulk'] = is_bulk
        return serializer_class(*args, **kwargs)

    def create(self, request, *args, **kwargs):
        if isinstance(request.data, list):
            return self.bulk_update(request)
        return super().create(request, *args, **kwargs)

    def bulk_update(self, request):
        queryset = self.filter_queryset(self.get_queryset())
        serializer = self.get_serializer(
            queryset,
            data=request.data,
        )
        serializer.is_valid(raise_exception=True)
        self.perform_update(serializer)
        return Response(serializer.data, status=status.HTTP_200_OK)

我已经测试过它是否有效,但我不知道它将如何影响 API 架构文档。