Django Rest Framework 6:视图集和路由器

导读:本篇文章讲解 Django Rest Framework 6:视图集和路由器,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一,视图集

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() 方法来实现,同理,UpdateModelMixinupdate 替换 putDestroyModelMixindestroy 替换 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) # DefaultRouter 将在其 URL 模式中配置包含 `pk` 的详细操作。
    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 装饰器匹配相同的参数。此外,此方法可用于反转默认操作,例如 listcreate

3,视图 VS 视图集

与使用 View 类相比,使用 ViewSet 类有两个主要优点。

  • 重复的逻辑可以组合成一个类。在上面的例子中,我们只需要指定一次 queryset,它将在多个视图中使用。
  • 通过使用 routers,不再需要自己处理URLconf。

这并不意味着视图集总是正确的方法,需要考虑一组类似的权衡:

  • 使用常规的 views 和 URL 能更明确的提供更多的控制。
  • ViewSets 有助于快速启动和运行,或者当你有大型的API,并且希望在整个过程中执行一致的 URL 配置。

(二)视图集 API 参考

1,ViewSet

ViewSet 类继承自 APIView。可以使用任何标准属性 (例如 permission_classesauthentication_classes等) 来控制视图集上的 API 策略。

它不提供任何操作的实现,通常需要子类化它并显式地定义操作的实现。

2,GenericViewSet

GenericViewSet继承自 GenericAPIView ,并提供默认的 get_objectget_queryset 方法和 GenericAPIView的其他基本行为,但默认情况下不包含任何操作。
通常需要子类化它,或混合所需的 mixin 类,或者显式定义操作的实现。

3,ModelViewSet

ModelViewSet 类也继承自 GenericAPIView,并通过混合各种 mixin 类的行为来包含各种操作的实现,它提供.list(),.retrieve(),.create(),.update(),.partial_update().destroy()的实现。

因为ModelViewSetextends继承自 GenericAPIView,所以在使用时需要提供querysetserializer_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 方法将被 listcreateretrieveupdatedestroy替代。
但 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 后:

  1. Django 确定使用根 URLconf 模块(由 ROOT_URLCONF 设置,通常是”djangoprojectname.urls”,但如果传入的 HttpRequest 对象提前被中间件设置了 urlconf 属性,这个值将被用来代替 ROOT_URLCONF 设置)。
  2. Django 加载该 Python 模块并寻找可用的 urlpatterns ,它是django.urls.path()django.urls.re_path() 实例的序列。
  3. Django 会按顺序遍历序列中的每个 URL 模式,然后会在所请求的URL 匹配到第一个模式后,并与HttpRequest 对象的 path_info 匹配。
  4. 一旦有 URL 匹配成功,Djagno 导入并调用相关的视图。
  5. 如果没有 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
  1. 实例化了一个路由器类 SimpleRouter
  2. 调用 register() 方法,将视图集 UserViewSet
  3. 注册到以 ‘users’ 开头的 URL 模式,自动生成一些 URL。
  4. 将这这些自动生成的 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 参数正确地反映命名空间:

# 方法一,让超链接到用户详细信息视图的序列化器字段的view_name参数。
	highlight = serializers.HyperlinkedIdentityField(view_name='v1:snippet-highlight', format='html')

# 方法二,在超链接序列化器 Mete 的 extra_kwargs 属性中设置view_name和lookup_field中的一个或两个。
        extra_kwargs = {
            'url': {'view_name': 'accounts', 'lookup_field': 'account_name'},
            'users': {'lookup_field': 'username'}	
        }

3,路由器与视图集中的额外操作

通常来说,路由器与视图集配合使用。尽管视图集已经相抽象,但难免不需要自定义一些方法,此时就需要一些装饰器来辅助路由器完成路由映射工作:

  • @detail_route
  • @list_route

举个例子🌰:

# 视图集
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_pathurl_name参数对自定义视图的 URL 生成进行额外的控制。具体参考视图集部分。

(三)API 参考

1,SimpleRouter

该路由器包括标准集合 list, create, retrieve, update, partial_updatedestroy 这几个 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'
})

# app URLconf 模块
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')
])

# 根 URLconf 模块不变
urlpatterns = [
    path('', include(('snippet.urls', 'snippet'), namespace='v1')),
    path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

第二种方法就是使用路由器,让系统自动为我们实现映射功能,我们需要做的就是向路由器注册适当的视图集到 URL:

# app URLconf 模块
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippet import views

# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets/v1', views.SnippetViewSet)
router.register(r'users/v1', views.UserViewSet)

# The API URLs are now determined automatically by the router.
urlpatterns = [
    path('', include(router.urls)),
]
  • 正在使用的DefaultRouter类会自动为我们创建 API 根视图,因此我们现在可以从 views 模块中删除 api_root 方法。

看下一效果:
在这里插入图片描述

(四)自定义路由器

路由器的用法相对固定,不太建议自定义路由器类,具体自定义过程,请参考Custom Routers

到这里,教程就结束啦!!!完结撒花!!!!🎉🎉🎉✨✨✨

在实际中要多加运用和积累呀!!!💪💪💪

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/98053.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!