一,视图集
REST framework 允许将一组相关类视图的逻辑组合在一个类中,称为 ViewSet 视图集。在其他框架中,你可能也会发现概念上类似的实现,比如“Resources”或“Controllers”。
ViewSet 可认为是一组类视图的抽象,因此它不提供任何诸如 .get() 或 .post() 这类特定的方法处理程序,而是提供诸如 .list() 和 .create() 之类的操作。
ViewSet 的方法处理程序只在结束视图时使用.as_view() 方法绑定到相应的操作。
通常,不需要在 URL 中显式地注册视图集,而是将视图集注册到一个Router 路由器中,它会自动为你确定的路由。
(一)视图集
1,为什么需要视图集
首先定义两个个独立的简单的类视图,分别获取用户列表和用户详情:
class UsersList(APIView):
def get(self, request, format=None):
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return Response(serializer.data)
class UserDetail(APIView):
def get_object(self, pk):
try:
return User.objects.get(pk=pk)
except User.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
user = self.get_object(pk)
serializer = UserSerializer(user)
return Response(serializer.data)
def put(self, request, pk, format=None):
user = self.get_object(pk)
serializer = UserSerializer(user, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
user = self.get_object(pk)
user.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
路由:
from MyAPP import views
urlpatterns = [
url(r'^userlist/$', views.UsersList.as_view()),
url(r'^userdetail/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
]
显然我们可以使用通用类视图和 Mixin 类来简化这两个类视图中处理不同请求方法的方法:
class UsersList(mixins.ListModelMixin,
generics.GenericAPIView):
queryset = Users.objects.all()
serializer_class = UsersSerializer
class UserDetail(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
路由:
from MyAPP import views
urlpatterns = [
url(r'^userlist/$', views.UsersList.as_view()),
url(r'^userdetail/(?P<pk>[0-9]+)/$', views.UserDetail.as_view()),
]
这样一来,我们将普通类视图中重写在 get() 方法中的逻辑交由 ListModelMixin 提供的 list() 方法来实现,同理,UpdateModelMixin 的 update 替换 put ,DestroyModelMixin 的 destroy 替换 delete ,等等,总之,通用视图类与Mixin类结合使用的结果就是将 http_methods 替换为 actions 。
尽管这两个类视图分别通过 actions 操作的是资源集合和单个资源,但对同一类资源的操作无非 CRUD,只是因为“惯性”,我们将它们分散在了不同的类视图中。
为此,我们可以进一步将这些 actions 抽象到一起,会比较方便管理,而这个抽象的手段就是 ViewSet 视图集。
现在定义一个简单的视图集:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
ModelViewSet 提供了对所有 actions 的支持。
使用视图集可能是一个非常有用的抽象。它最大限度地减少需要编写的代码量。
但却产生了一个新问题:如何进行 URL 映射?
2,路由器 Routers 与视图集中的额外操作
如果我们需要,我们仍然可以在 URL 中将这个视图集绑定到两个单独的视图,想这样:
user_list = UserViewSet.as_view({'get': 'list'})
user_detail = UserViewSet.as_view({'get': 'retrieve'})
通常我们不会这么做,只需要用一个路由器来注册我们的视图集,然后就能自动完成 URL 映射而不用再使用标准路由系统:
from myapp.views import UserViewSet
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'users', UserViewSet)
urlpatterns = router.urls
通过使用路由器,我们不再需要自己写路由映射。
REST framework 中内置的DefaultRouter 将为一套标准的 CRUD 操作提供路由,如下所示:
class UserViewSet(viewsets.ViewSet):
"""
Example empty viewset demonstrating the standard
actions that will be handled by a router class.
If you're using format suffixes, make sure to also include
the `format=None` keyword argument for each action.
"""
def list(self, request):
pass
def create(self, request):
pass
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
def destroy(self, request, pk=None):
pass
如果有需要映射的其它方法,可以用 @action 装饰器来标记它们。与常规 actions 一样,额外的 actions 也可用于对象列表或单个实例。
为标明这一点,需要设置 detail 参数为 True 或 False。路由器将相应地配置其 URL 模式。
更完整的额外 actions 的例子:
from django.contrib.auth.models import User
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from myapp.serializers import UserSerializer, PasswordSerializer
class UserViewSet(viewsets.ModelViewSet):
"""
提供标准操作的视图集
"""
queryset = User.objects.all()
serializer_class = UserSerializer
@action(methods=['post'], detail=True)
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
if serializer.is_valid():
user.set_password(serializer.data['password'])
user.save()
return Response({'status': 'password set'})
else:
return Response(serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
@action(detail=False)
def recent_users(self, request):
recent_users = User.objects.all().order('-last_login')
page = self.paginate_queryset(recent_users)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(recent_users, many=True)
return Response(serializer.data)
另外,装饰器可以为路由视图设置额外的参数:
@action(methods=['post'], detail=True, permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
@action 装饰器默认路由 GET 请求,但也可以通过设置 methods 参数来接受其他 HTTP 方法:
@action(methods=['post', 'delete'], detail=True)
def unset_password(self, request, pk=None):
...
当然也能用前面提到的 @detail_route 或 @list_route 来一定程度上代替 @action 。
如果你要查看所有额外操作,请调用 .get_extra_actions() 方法。
如果你需要获取操作的 URL,请使用 .reverse_action() 方法。这是 Reverse() 的便捷封装,它自动传递视图的 request 对象,并在 url_name 前加上 .basename 属性。 请注意,basename 是路由器在视图集注册期间提供的。如果不使用路由器,则必须向 .as_view() 方法提供 basename 参数:
>>> view.reverse_action('set-password', args=['1'])
'http://localhost:8000/api/users/1/set_password'
或者使用 @action 装饰器设置的 url_name 属性:
>>> view.reverse_action(view.set_password.url_name, args=['1'])
'http://localhost:8000/api/users/1/set_password'
.reverse_action() 的 url_name 参数应该与 @action 装饰器匹配相同的参数。此外,此方法可用于反转默认操作,例如 list 和 create 。
3,视图 VS 视图集
与使用 View 类相比,使用 ViewSet 类有两个主要优点。
- 重复的逻辑可以组合成一个类。在上面的例子中,我们只需要指定一次 queryset,它将在多个视图中使用。
- 通过使用 routers,不再需要自己处理URLconf。
这并不意味着视图集总是正确的方法,需要考虑一组类似的权衡:
- 使用常规的 views 和 URL 能更明确的提供更多的控制。
- ViewSets 有助于快速启动和运行,或者当你有大型的API,并且希望在整个过程中执行一致的 URL 配置。
(二)视图集 API 参考
1,ViewSet
ViewSet 类继承自 APIView 。可以使用任何标准属性 (例如 permission_classes ,authentication_classes 等) 来控制视图集上的 API 策略。
它不提供任何操作的实现,通常需要子类化它并显式地定义操作的实现。
2,GenericViewSet
GenericViewSet 继承自 GenericAPIView ,并提供默认的 get_object ,get_queryset 方法和 GenericAPIView 的其他基本行为,但默认情况下不包含任何操作。 通常需要子类化它,或混合所需的 mixin 类,或者显式定义操作的实现。
3,ModelViewSet
ModelViewSet 类也继承自 GenericAPIView ,并通过混合各种 mixin 类的行为来包含各种操作的实现,它提供.list(),.retrieve(),.create(),.update(),.partial_update() 和 .destroy() 的实现。
因为ModelViewSetextends 继承自 GenericAPIView ,所以在使用时需要提供queryset 和serializer_class 属性
class AccountViewSet(viewsets.ModelViewSet):
"""
用于查看和编辑帐户的简单 ViewSet。
"""
queryset = Account.objects.all()
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
注意,可以使用 GenericAPIView 提供的任何标准属性或方法重写。例如,要动态确定视图集应该操作的queryset ,则可以重写 get_queryset() 方法:
class AccountViewSet(viewsets.ModelViewSet):
"""
一个简单的视图集,用于查看和编辑与用户相关的帐户。
"""
serializer_class = AccountSerializer
permission_classes = [IsAccountAdminOrReadOnly]
def get_queryset(self):
return self.request.user.accounts.all()
前面讲到,路由器注册时必须有个依据来指定 name ,当从视图集中删除 queryset 属性后,任何关联的路由器将可能无法自动导出模型的 base_name ,因此您必须指定 base_name 作为路由器注册的一部分。
回到教程👨?💻。 这里就来使用ModelViewSet 统一几处与 Snippet 相关的分散的类视图,其中 get、post 和 delete 方法将被 list ,create ,retrieve ,update 和 destroy 替代。 但 SnippetHighlight 中的 get 的操作并逻辑不是标准的create /update /delete actions,它只获取 Snippet 模型的一个字段的信息,因此应该作为一个额外操作独立出来,并接受@action 的装饰。 同时,为了依然能在正式保存数据之前添加 owner 数据,perform_create() 方法会得到保留,但不作为一个额外操作,因为它重写自 ModelViewSet 所继承的 CreateModelMixin 。
...
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import permissions
class SnippetViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
Additionally we also provide an extra `highlight` action.
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]
@action(detail=True,renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
...
4,ReadOnlyModelViewSet
ReadOnlyModelViewSet 类也继承自 GenericAPIView 。它只提供只读操作.list() 和 .retrieve() 。
class AccountViewSet(viewsets.ReadOnlyModelViewSet):
"""
用于查看帐户的简单 ViewSet。
"""
queryset = Account.objects.all()
serializer_class = AccountSerializer
回到教程👨?💻。
考虑到我们完全不希望在除了后台之外的地方设置用户信息,所以我们直接使用ReadOnlyModelViewSet 来抽象所有与用户相关的类视图:
...
from rest_framework import viewsets
...
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `retrieve` actions.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
最后,这些内置视图集的关系如下:
(三)自定义 ViewSet 基类
如果需要提供现有内置视图集之外的其他方式自定义操作,一个方法是继承它们,并提供实现,另一种方法就是继承GenericViewSet 并完全自定义实现,或者结合一些mixin 类。 举个例子🌰:
from rest_framework import mixins
class CreateListRetrieveViewSet(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet):
"""
A viewset that provides `retrieve`, `create`, and `list` actions.
To use it, override the class and set the `.queryset` and
`.serializer_class` attributes.
"""
pass
二,路由器
(一)django中的路由系统
django中的路由系统又叫做 URL 调度器。
当一个用户请求到达 django 后:
- Django 确定使用根 URLconf 模块(由
ROOT_URLCONF 设置,通常是"djangoprojectname.urls",但如果传入的 HttpRequest 对象提前被中间件设置了 urlconf 属性,这个值将被用来代替 ROOT_URLCONF 设置)。 - Django 加载该 Python 模块并寻找可用的
urlpatterns ,它是django.urls.path() 与 django.urls.re_path() 实例的序列。 - Django 会按顺序遍历序列中的每个 URL 模式,然后会在所请求的URL 匹配到第一个模式后,并与
HttpRequest 对象的 path_info 匹配。 - 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图。
- 如果没有 URL 被匹配,或者匹配过程中出现了异常,Django 会调用一个适当的错误处理视图。
在这个 URL 调度过程中,还能实现更详细的工作:
- 将 URL 从根 URLconf 模块分配到具体的应用 URLconf 模块。
- 在 URL 中传递额外参数给视图。
- 配合模板里的
url 标签、视图中的 reverse() 等函数、模型中的 get_absolute_url() 方法来反向解析 URL。
总之,路由系统的工作就是实现 URL 与视图的正确映射。
(二)Routers in the Rest Framework
Rest Framework 支持自动映射 URL 到视图,并为此提供了一种简单、快速和一致的方式——路由器Routers。
1,使用示例
举个例子🌰:
from rest_framework import routers
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
router.register(r'accounts', AccountViewSet)
urlpatterns = router.urls
- 实例化了一个路由器类
SimpleRouter 。 - 调用
register() 方法,将视图集 UserViewSet - 注册到以 ‘users’ 开头的 URL 模式,自动生成一些 URL。
- 将这这些自动生成的 URL 赋值给
urlpatterns ,等待被请求匹配。
实际上register() 方法还允许提供可选的第三个参数:base_name ,目的就是手动指定l类似普通路由的 name 参数的值——这个值是必须的,没有 base_name 时由queryset 属性代替,连queryset 都没有时必须手动指定base_name 。
上面的这个示例将生成以下URL模式:
- URL pattern: ^users/$ Name: ‘user-list’
- URL pattern: ^users/{pk}/$ Name: ‘user-detail’
- URL pattern: ^accounts/$ Name: ‘account-list’
- URL pattern: ^accounts/{pk}/$ Name: ‘account-detail’
URL 模式的开头由 register() 方法的第一个参数指定,会被映射到由 register() 方法的第二个参数指定的视图集的 actions,URL Name 的开头由 register() 方法的第三个参数指定。
当然,这只是最基础的用法,类似于:
re_path(r'^snippets/v1/$', views.SnippetList.as_view(), name='snippet-list'),
但 urls 确实就返回了这种最简单但标准的 URL 。
2,路由器与命名空间
我们可以像这样将自动生成的路由附加到路由列表中:
router = routers.SimpleRouter()
router.register(r'users', UserViewSet)
router.register(r'accounts', AccountViewSet)
urlpatterns = [
url(r'^forgot-password/$', ForgotPasswordFormView.as_view()),
]
urlpatterns += router.urls
如果要将路由转移到一个 URLconf 模块,同样能像标准 URL 那样使通用 include() 函数来实现:
urlpatterns = [
url(r'^forgot-password/$', ForgotPasswordFormView.as_view()),
url(r'^', include(router.urls)),
]
都到了能转移路由的程度了,自然也应该支持命名空间:
urlpatterns = [
url(r'^forgot-password/$', ForgotPasswordFormView.as_view()),
url(r'^api/', include(router.urls, namespace='api')),
]
如果使用带超链接序列化器的命名空间,你还需要确保序列化器上的任何 view_name 参数正确地反映命名空间:
highlight = serializers.HyperlinkedIdentityField(view_name='v1:snippet-highlight', format='html')
extra_kwargs = {
'url': {'view_name': 'accounts', 'lookup_field': 'account_name'},
'users': {'lookup_field': 'username'}
}
3,路由器与视图集中的额外操作
通常来说,路由器与视图集配合使用。尽管视图集已经相抽象,但难免不需要自定义一些方法,此时就需要一些装饰器来辅助路由器完成路由映射工作:
举个例子🌰:
from myapp.permissions import IsAdminOrIsSelf
from rest_framework.decorators import detail_route
class UserViewSet(ModelViewSet):
...
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
...
将另外生成以下 URL 模式:
URL pattern: ^users/{pk}/set_password/$ Name: 'user-set-password'
如果不想让自定义的额外操作使用自动生成的默认 URL,则可以用装饰器中的url_path 参数进行自定义。举个例子🌰:
from myapp.permissions import IsAdminOrIsSelf
from rest_framework.decorators import detail_route
class UserViewSet(ModelViewSet):
...
@detail_route(methods=['post'], permission_classes=[IsAdminOrIsSelf], url_path='change-password')
def set_password(self, request, pk=None):
...
以上示例现在将生成以下 URL 格式:
URL pattern: ^users/{pk}/set_password/$ Name: 'user-change-password'
还可以同时设置url_path 和url_name 参数对自定义视图的 URL 生成进行额外的控制。具体参考视图集部分。
(三)API 参考
1,SimpleRouter
该路由器包括标准集合 list , create , retrieve , update , partial_update 和 destroy 这几个 actions 的路由。视图集中还可以使用@detail_route 或@list_route 装饰器标记要被路由的其他方法。
该路由器产生如下格式的 URL。
2,DefaultRouter
该路由器类继承自SimpleRouter ,添加了默认 API 根视图,并可以将格式后缀模式添加到 URL。
回到教程👨?💻。 在前面,我们已经将类视图替换为视图集,接下来要做的,自然就是实现路由映射了。 第一种方法,我们还是显式地将路由映射到一组具体的视图中去:先将请求方法与对应的 action 操作通过视图集的 as_view() 方法关联起来,然后挨个放入 app URLconf 模块,这样做的一个好处是能指定 URL 模式(比如显式添加版本号);
from snippets.views import SnippetViewSet, UserViewSet, api_root
from rest_framework import renderers
snippet_list = SnippetViewSet.as_view({
'get': 'list',
'post': 'create'
})
snippet_detail = SnippetViewSet.as_view({
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
})
snippet_highlight = SnippetViewSet.as_view({
'get': 'highlight'
}, renderer_classes=[renderers.StaticHTMLRenderer])
user_list = UserViewSet.as_view({
'get': 'list'
})
user_detail = UserViewSet.as_view({
'get': 'retrieve'
})
urlpatterns = format_suffix_patterns([
path('', api_root),
path('snippets/v1/', snippet_list, name='snippet-list'),
path('snippets/v1/<int:pk>/', snippet_detail, name='snippet-detail'),
path('snippets/v1/<int:pk>/highlight/', snippet_highlight, name='snippet-highlight'),
path('users/v1/', user_list, name='user-list'),
path('users/v1/<int:pk>/', user_detail, name='user-detail')
])
urlpatterns = [
path('', include(('snippet.urls', 'snippet'), namespace='v1')),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
第二种方法就是使用路由器,让系统自动为我们实现映射功能,我们需要做的就是向路由器注册适当的视图集到 URL:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippet import views
router = DefaultRouter()
router.register(r'snippets/v1', views.SnippetViewSet)
router.register(r'users/v1', views.UserViewSet)
urlpatterns = [
path('', include(router.urls)),
]
- 正在使用的
DefaultRouter 类会自动为我们创建 API 根视图,因此我们现在可以从 views 模块中删除 api_root 方法。
看下一效果:
(四)自定义路由器
路由器的用法相对固定,不太建议自定义路由器类,具体自定义过程,请参考Custom Routers。
到这里,教程就结束啦!!!完结撒花!!!!🎉🎉🎉???
在实际中要多加运用和积累呀!!!💪💪💪
|