18. Django rest framework 视图集
回顾之前的内容, 通过通用视图对接口进行了简化, 最终得到了代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ProductList(generics.ListCreateAPIView):
serializer_class = ProductSerializer
queryset = Product.objects.select_related('collection').all()
class ProductDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def delete(self, request, *args, **kwargs):
product = self.get_object()
if product.orderitem_set.count() > 0:
return Response({'error': 'Product has been ordered.'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
这两个类相较最初的视图方法和视图类已经进行了极大的简化, 但是任然存在重复的代码或者模式.
事实上, select_related('collection')
也仅仅只是为了测试字符串关联查询而临时加上去的. 所以两个类真正不同的地方仅有delete
函数而已.
REST Framework针对这种情况提供了另外一种解决方案, 视图集, 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from rest_framework.viewsets import ModelViewSet
class ProductViewSet(ModelViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
def delete(self, request, *args, **kwargs):
product = self.get_object()
if product.orderitem_set.count() > 0:
return Response({'error': 'Product has been ordered.'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
仅需要更改一下父类, 并把代码进行简单和并即可, 而查看ModelViewSet
的源码可以发现, 其本质也是一个包含多种Mixin
的通用视图:
1
2
3
4
5
6
7
8
9
10
11
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
路由设置
当使用了视图集时, 对于接口路由的配置也需要进行对应的改动, 不再需要手动的设置接口对应的具体视图与名称, 而是交由Router
进行自动设置. 代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import generic_api
router = DefaultRouter()
router.register('products', generic_api.ProductViewSet)
router.register('collectons', generic_api.CollectionViewSet)
urlpatterns = [
path('', include(router.urls)),
# other path you need.
]
只需简单几个步骤:
- 导入
DefaultRouter
, 并创建对象 - 注册视图名与对应的视图集
- 在urlpatterns中包含生成的路由配置
查看router.urls
可以看到, 我们已经得到了一个完整的路由配置列表, 与之前的配置功能一样, 并自动为每一个路由生成了名称.
1
2
3
4
5
6
[
<URLPattern '^products/$' [name='product-list']>,
<URLPattern '^products/(?P<pk>[^/.]+)/$' [name='product-detail']>,
<URLPattern '^collectons/$' [name='collection-list']>,
<URLPattern '^collectons/(?P<pk>[^/.]+)/$' [name='collection-detail']>
]
除了自动完成了具体的路由配置之外, DefaultRouter还会配置一个根目录路由, 并在访问app是提供一个页面:
如果不想要这个页面, 也可以使用
SimpleRouter
.
关于视图集DELETE方法的一个大坑
在尝试将通用视图改为视图集的实现方式是, 可以正确的执行查询和更新操作, 但是在删除时出现了问题:
即删除逻辑并没有执行, 而是直接调用了序列化对象的delete()
方法, 导致直接抛出了依赖保护异常.
在将配置文件切换回通用视图后正常, 转到视图集后便出现问题. 查询源码发现了问题所在.
在通过APIView
方式处理时, 代码如下:
1
2
3
4
5
6
class ProductDetail(generics.RetrieveUpdateDestroyAPIView):
# code
def delete(self, request, *args, **kwargs):
# logic
return Response(status=status.HTTP_204_NO_CONTENT)
通过查看 RetrieveUpdateDestroyAPIView
类的源码:
1
2
3
4
5
6
7
8
9
10
11
class RetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
GenericAPIView):
# other method
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
可以看到, 父类的delete
方法调用的是self.destroy()
方法, 而这个destroy
方法则是destroyModelMixin
所定义的方法
1
2
3
4
5
6
7
8
9
10
11
class DestroyModelMixin:
"""
Destroy a model instance.
"""
def destroy(self, request, *args, **kwargs):
instance = self.get_object()
self.perform_destroy(instance)
return Response(status=status.HTTP_204_NO_CONTENT)
def perform_destroy(self, instance):
instance.delete()
再看ModelViewSet
类的源码:
1
2
3
4
5
6
7
8
9
10
11
12
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
这或直接写了个pass, 也就是说自定义的ViewSet
不会重写父类的delete
方法, 因为父类就没有delete
方法.
而ViewSet
能够执行删除操作, 显然是交由路由配置之后直接调用了destroy()
方法.
所以之前在ViewSet
中定义的delete
方法虽然有判断逻辑, 但是却被直接跳过了.
解决方式也很简单, 将ViewSet
中的delete
方法名改为destroy
即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
class ProductViewSet(ModelViewSet):
serializer_class = ProductSerializer
queryset = Product.objects.all()
def destroy(self, request, *args, **kwargs):
product = self.get_object()
print(product)
if product.orderitem_set.count() > 0:
return Response({'error': 'Product has been ordered.'},
status=status.HTTP_405_METHOD_NOT_ALLOWED)
product.delete()
return Response(status=status.HTTP_204_NO_CONTENT)