尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

DRF视图与路由深度解析:从APIView到ViewSet的RESTful API构建实践

DRF视图与路由深度解析:从APIView到ViewSet的RESTful API构建实践 1. 项目概述从Django到DRF的视图与路由跃迁如果你已经用Django写过几个项目对MTVModel-Template-View模式滚瓜烂熟那么初次接触Django REST framework时可能会感到一丝“熟悉的陌生感”。视图View和路由URLconf这两个老朋友还在但它们的玩法和内涵已经发生了深刻的变化。在传统的Django项目中视图函数或类视图的核心任务是接收一个HTTP请求处理业务逻辑然后返回一个渲染好的HTML模板响应。但在DRF构建的API世界里视图的使命变成了接收请求、解析数据、执行序列化/反序列化、进行权限校验最后返回结构化的JSON或其他格式数据。路由也不再仅仅是URL到视图的简单映射它需要与DRF的视图集ViewSet和路由器Router深度配合实现API端点的自动生成与组织。简单来说DRF的视图和路由是专门为构建优雅、规范、高效的RESTful API而设计的增强工具包。它们封装了大量通用逻辑让你能摆脱重复的CRUD代码专注于业务本身。但与此同时它们也引入了一套新的概念和约定比如APIView、GenericAPIView、ViewSet、Router等。理解并掌握这些概念是能否用好DRF的关键。本文将深入DRF视图与路由的核心不仅告诉你“怎么用”更会剖析“为什么这么设计”并分享我在实际项目中积累的配置心得与避坑经验。2. DRF视图体系的三大支柱APIView, GenericAPIView, ViewSetDRF的视图类并非一个单一的存在而是一个层次分明、功能递进的体系。理解这个体系你就能根据不同的场景选择最合适的工具而不是盲目地使用最“高级”的那个。2.1 APIView一切的基础rest_framework.views.APIView是DRF所有视图类的基类它继承自Django的View类。你可以把它理解为DjangoView的“RESTful升级版”。核心增强功能请求与响应对象APIView将Django原生的HttpRequest对象封装为DRF的Request对象。这个新对象最实用的特性是.data属性它能自动根据Content-Type头如application/json解析请求体返回一个Python字典你再也不用手动去json.loads(request.body)了。同样Response对象可以帮你自动将Python原生数据类型如dict, list序列化为JSON并设置合适的Content-Type。身份认证与权限检查通过authentication_classes和permission_classes类属性你可以轻松地为视图配置认证方案如Token、Session、JWT和权限策略如IsAuthenticated、IsAdminUser。这些检查会在进入具体的处理方法如get,post之前自动执行。流量限制可以通过throttle_classes来配置访问频率限制防止API被滥用。内容协商自动根据客户端请求的Accept头决定返回数据的渲染格式JSON、XML等。一个典型的APIView示例from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import Book from .serializers import BookSerializer class BookListAPIView(APIView): 处理 /api/books/ 的GET和POST请求 def get(self, request): # 获取所有图书 books Book.objects.all() # 使用序列化器将QuerySet转换为JSON格式数据 serializer BookSerializer(books, manyTrue) # 返回Response对象DRF会自动处理序列化 return Response(serializer.data) def post(self, request): # request.data 是已经解析好的字典 serializer BookSerializer(datarequest.data) if serializer.is_valid(): serializer.save() # 创建成功返回201状态码和创建的数据 return Response(serializer.data, statusstatus.HTTP_201_CREATED) # 数据无效返回400状态码和错误详情 return Response(serializer.errors, statusstatus.HTTP_400_BAD_REQUEST)注意APIView给了你最大的灵活性你需要手动编写每一个HTTP方法对应的逻辑。对于简单的、非标准的端点它非常合适。但当你要实现标准的CRUDCreate, Retrieve, Update, Delete, List操作时重复代码会开始显现。2.2 GenericAPIView通用逻辑的抽象rest_framework.generics.GenericAPIView继承自APIView。它的设计哲学是将视图中最常见的模式如获取一个数据集、获取单个对象抽象出来通过组合“Mixin”类来快速构建功能。它提供了哪些通用属性queryset指定这个视图所要操作的数据集一个Django QuerySet。serializer_class指定用于序列化和反序列化的序列化器类。lookup_field用于检索单个对象时模型字段的名称默认是pk。lookup_url_kwargURL conf中对应的参数名默认与lookup_field相同。但GenericAPIView本身不实现任何HTTP方法。它的威力需要与Mixin类结合才能发挥。DRF提供了一系列MixinListModelMixin提供.list(request, *args, **kwargs)方法用于列出资源集合。CreateModelMixin提供.create(request, *args, **kwargs)方法用于创建资源。RetrieveModelMixin提供.retrieve(request, *args, **kwargs)方法用于获取单个资源。UpdateModelMixin提供.update(request, *args, **kwargs)和.partial_update(...)方法用于完整更新和部分更新。DestroyModelMixin提供.destroy(request, *args, **kwargs)方法用于删除资源。组合使用示例from rest_framework import generics, mixins from .models import Book from .serializers import BookSerializer # 组合生成一个“列表”和“创建”视图 class BookListCreateView(mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView): queryset Book.objects.all() serializer_class BookSerializer def get(self, request, *args, **kwargs): # 调用ListModelMixin的list方法 return self.list(request, *args, **kwargs) def post(self, request, *args, **kwargs): # 调用CreateModelMixin的create方法 return self.create(request, *args, **kwargs)可以看到我们仍然需要手动将HTTP方法get,post映射到Mixin提供的方法。这引出了下一层封装。2.3 具体的通用类视图开箱即用的CRUDDRF预置了组合好的通用类视图它们是GenericAPIView和各种Mixin的“快捷方式”你不需要再手动写get、post等方法了。ListAPIViewGenericAPIViewListModelMixinCreateAPIViewGenericAPIViewCreateModelMixinRetrieveAPIViewGenericAPIViewRetrieveModelMixinUpdateAPIViewGenericAPIViewUpdateModelMixinDestroyAPIViewGenericAPIViewDestroyModelMixinListCreateAPIView 以上列表和创建的组合RetrieveUpdateAPIView 获取和更新的组合RetrieveDestroyAPIView 获取和删除的组合RetrieveUpdateDestroyAPIView 获取、更新、删除的组合使用示例from rest_framework import generics from .models import Book from .serializers import BookSerializer class BookListView(generics.ListCreateAPIView): 处理GET列表和POST创建 queryset Book.objects.all() serializer_class BookSerializer class BookDetailView(generics.RetrieveUpdateDestroyAPIView): 处理GET单个、PUT全更新、PATCH部分更新、DELETE queryset Book.objects.all() serializer_class BookSerializer代码变得极其简洁你只需要定义queryset和serializer_class标准的CRUD行为就已经实现了。这是DRF生产力提升的核心体现。实操心得对于标准的模型资源API我几乎总是从generics.ListCreateAPIView和generics.RetrieveUpdateDestroyAPIView开始。它们覆盖了95%的需求。只有在需要实现非标准逻辑如复杂的多条件查询、特殊的创建流程时才会退回到APIView或GenericAPIView与mixins的组合。2.4 ViewSet将视图组织为资源集合ViewSet是DRF视图体系的另一个抽象层次。它的核心思想是将一组相关的视图逻辑通常是针对同一个模型的所有操作组织在一个类里。ViewSet类本身不提供任何动作它继承自APIView。GenericViewSet继承自GenericAPIView它提供了get_object,get_serializer等通用方法但同样不绑定HTTP方法。ModelViewSet是最常用的它继承了GenericViewSet并一次性混入了所有的MixinList, Create, Retrieve, Update, Destroy为模型提供了完整的CRUD操作。ModelViewSet示例from rest_framework import viewsets from .models import Book from .serializers import BookSerializer class BookViewSet(viewsets.ModelViewSet): 一个ViewSet自动提供list, create, retrieve, update, partial_update, destroy 动作。 queryset Book.objects.all() serializer_class BookSerializer代码和GenericAPIView的组合体一样简洁。但关键区别在于ViewSet本身不直接绑定到URL。它需要通过路由器Router来生成URL配置这是下一节的重点。为什么需要ViewSet逻辑组织将所有针对“图书”的操作放在BookViewSet一个类里比分散在BookListView、BookDetailView等多个类中更符合“资源”的RESTful思想。路由自动化配合路由器可以自动生成标准的RESTful URL如/books/,/books/{id}/极大减少urls.py中的重复代码。额外动作你可以在ViewSet中轻松定义非标准的“动作”Action例如为图书添加一个“借阅”或“点赞”的端点。3. 路由配置从手动映射到自动生成在Django中我们在urls.py里使用path()或re_path()手动将URL模式映射到视图函数或类视图的as_view()方法。在DRF中对于APIView和GenericAPIView我们依然这样做。3.1 传统方式手动映射通用视图# urls.py from django.urls import path from .views import BookListView, BookDetailView urlpatterns [ path(books/, BookListView.as_view(), namebook-list), path(books/int:pk/, BookDetailView.as_view(), namebook-detail), ]这种方式清晰直接对于简单的、数量不多的API端点完全够用。3.2 路由器RouterViewSet的绝配当使用ViewSet特别是ModelViewSet时DRF的Router类可以帮你自动生成上述URL配置。基本使用# urls.py (项目根目录或app目录) from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import BookViewSet # 创建一个路由器并注册我们的ViewSet router DefaultRouter() router.register(rbooks, BookViewSet, basenamebook) # 将路由器生成的URL包含进来 urlpatterns [ path(api/, include(router.urls)), ]执行上述代码后路由器会自动生成以下URL模式^api/books/$- 对应BookViewSet的list(GET) 和create(POST) 动作。^api/books/{pk}/$- 对应BookViewSet的retrieve(GET),update(PUT),partial_update(PATCH),destroy(DELETE) 动作。DefaultRouter还会自动为你创建一个API根视图列出所有已注册的API端点访问/api/即可看到非常方便。3.3 自定义ViewSet中的额外动作ActionViewSet的强大之处在于可以方便地添加非标准的端点。例如为图书添加一个“标记为已读”的端点。使用action装饰器from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from .models import Book class BookViewSet(viewsets.ModelViewSet): queryset Book.objects.all() serializer_class BookSerializer # detailTrue 表示这个动作是针对单个对象的/books/{pk}/mark_as_read/ # detailFalse 则表示是针对集合的/books/mark_all_as_read/ action(detailTrue, methods[post]) def mark_as_read(self, request, pkNone): book self.get_object() # GenericViewSet提供的便捷方法 book.is_read True book.save() # 可以使用另一个序列化器来返回数据 serializer self.get_serializer(book) return Response(serializer.data) action(detailFalse, methods[get]) def recent(self, request): # 获取最近出版的5本书 recent_books self.get_queryset().order_by(-publish_date)[:5] serializer self.get_serializer(recent_books, manyTrue) return Response(serializer.data)路由器会自动为这些用action装饰的方法生成对应的URLPOST /api/books/{pk}/mark_as_read/GET /api/books/recent/避坑经验action装饰器默认生成的URL路径是方法名本身如mark_as_read。你可以通过action(detailTrue, methods[post], url_pathcustom-path)中的url_path参数来自定义路径。另外url_name参数可以用于反向解析URL时指定名称。4. 视图与路由的进阶配置与性能考量掌握了基础之后我们来看看在实际项目中如何让视图和路由更加强大和高效。4.1 动态获取queryset和serializer_class很多时候queryset和serializer_class不是一成不变的。例如根据用户权限返回不同的数据集或者根据请求方法使用不同的序列化器。覆盖get_queryset方法class BookViewSet(viewsets.ModelViewSet): # 不再直接定义 queryset # queryset Book.objects.all() serializer_class BookSerializer def get_queryset(self): 动态返回QuerySet。 例如普通用户只能看到已发布的图书管理员可以看到所有。 user self.request.user if user.is_staff: return Book.objects.all() # 假设Book模型有一个 is_published 字段 return Book.objects.filter(is_publishedTrue)覆盖get_serializer_class方法class BookViewSet(viewsets.ModelViewSet): queryset Book.objects.all() # 不再直接定义单一的serializer_class def get_serializer_class(self): 为不同的动作使用不同的序列化器。 if self.action list: # 列表页使用一个简化的序列化器 return BookListSerializer elif self.action create: # 创建时需要更多字段验证 return BookCreateSerializer # 默认情况 return BookDetailSerializer4.2 权限与认证的细粒度控制DRF的权限系统非常灵活可以在全局设置、视图类级别、甚至视图方法级别进行控制。视图级别的权限设置from rest_framework.permissions import IsAuthenticated, IsAdminUser, DjangoModelPermissions class BookViewSet(viewsets.ModelViewSet): queryset Book.objects.all() serializer_class BookSerializer # 只有认证用户才能访问此ViewSet的所有端点 permission_classes [IsAuthenticated] # 可以进一步为特定动作设置不同的权限 action(detailTrue, methods[post], permission_classes[IsAdminUser]) def publish(self, request, pkNone): # 只有管理员可以调用发布动作 ...自定义权限类当内置权限类不满足需求时可以创建自定义权限类。from rest_framework import permissions class IsOwnerOrReadOnly(permissions.BasePermission): 自定义权限对象的所有者可以编辑其他用户只能查看。 def has_object_permission(self, request, view, obj): # 读取权限对任何请求都允许GET, HEAD, OPTIONS if request.method in permissions.SAFE_METHODS: return True # 写入权限只授予对象的所有者 # 假设对象有一个 owner 字段关联到User模型 return obj.owner request.user然后在视图中使用permission_classes [IsAuthenticated, IsOwnerOrReadOnly]。4.3 过滤、搜索与排序对于列表接口过滤、搜索和排序是刚需。DRF通过django-filter库和其自带的SearchFilter、OrderingFilter提供了强大支持。配置示例from django_filters.rest_framework import DjangoFilterBackend from rest_framework import filters, viewsets class BookViewSet(viewsets.ModelViewSet): queryset Book.objects.all() serializer_class BookSerializer # 配置过滤器后端 filter_backends [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] # 指定可过滤的字段 filterset_fields [author, publish_year, is_published] # 指定可搜索的字段 search_fields [title, author__name, description] # 指定可排序的字段 ordering_fields [publish_date, price, title] ordering [-publish_date] # 默认排序配置好后客户端就可以通过查询参数来使用这些功能过滤GET /api/books/?author鲁迅publish_year2023搜索GET /api/books/?search战争(会在title,author__name,description中搜索)排序GET /api/books/?orderingprice(升序) 或?ordering-price(降序)性能提示search_fields中使用双下划线跨关系查询如author__name可能会在数据量大时导致性能问题需要确保数据库相关字段已建立索引。对于复杂的过滤需求建议使用django-filter的FilterSet类进行更精确的控制。4.4 分页配置当数据量很大时必须对列表接口进行分页。DRF提供了几种分页样式。全局配置在settings.py中REST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20 }视图级别配置from rest_framework.pagination import PageNumberPagination from rest_framework import viewsets class LargeResultsSetPagination(PageNumberPagination): page_size 100 page_size_query_param page_size # 允许客户端通过 ?page_size50 指定每页大小 max_page_size 1000 # 每页最大数量限制 class BookViewSet(viewsets.ModelViewSet): queryset Book.objects.all() serializer_class BookSerializer pagination_class LargeResultsSetPagination除了PageNumberPagination?page2还有LimitOffsetPagination?limit20offset40和CursorPagination基于游标适用于无限滚动对大数据集性能更好可供选择。5. 实战中的常见问题与排查思路即使理解了原理在实际编码和调试中依然会遇到各种问题。下面分享几个我踩过的坑及其解决方案。5.1 视图返回AttributeError: ‘QuerySet‘ object has no attribute ‘pk‘问题场景在RetrieveAPIView、UpdateAPIView或RetrieveUpdateDestroyAPIView中你可能会遇到这个错误。根因分析这类视图以及对应的Mixin如RetrieveModelMixin依赖于get_object()方法来获取单个模型实例。get_object()方法默认使用URL conf中捕获的主键pk值并调用self.queryset.filter(pkpk)。如果你的queryset属性定义的不是一个简单的Model.objects.all()而是一个经过复杂过滤或注解annotate的QuerySet并且这个QuerySet在执行filter(pk...)时因为某些原因如聚合、错误的连接导致返回的不是模型实例而是一个QuerySet对象或其他对象就会触发这个错误。解决方案检查get_queryset()方法确保它返回的是一个标准的、可以正常执行.filter(pk...)和.get()的QuerySet。避免在其中进行会导致无法获取单个对象的操作。覆盖get_object()方法如果逻辑复杂直接覆盖它。class BookDetailView(generics.RetrieveUpdateDestroyAPIView): queryset Book.objects.all() serializer_class BookSerializer def get_object(self): # 自定义获取对象的逻辑例如基于slug而不是pk queryset self.filter_queryset(self.get_queryset()) obj get_object_or_404(queryset, slugself.kwargs[slug]) self.check_object_permissions(self.request, obj) return obj记得在urls.py中也要将int:pk改为slug:slug。5.2 路由器Router注册后URL不生效或404问题场景你已经用router.register()注册了ViewSet并将router.urls包含到了urlpatterns中但访问端点时返回404。排查步骤检查include路径确认path(api/, include(router.urls))中的前缀api/是否正确访问时是否加上了这个前缀如/api/books/。检查basename参数在router.register()时如果ViewSet类没有设置queryset属性或者你想覆盖默认的basename就必须显式提供basename参数。否则路由器可能无法为视图自动生成视图名称view name进而影响URL反向解析但通常不影响直接访问。不过在某些复杂情况下缺少basename可能导致问题。一个良好的习惯是如果ViewSet类定义了queryset属性可以不传basename如果没有定义则必须传。# ViewSet中没有定义queryset class BookViewSet(viewsets.ViewSet): def list(self, request): ... router.register(rbooks, BookViewSet, basenamebook) # 必须提供basename检查项目根urls.py确保你的app的urls.py被正确包含到了项目根目录的urlpatterns中。使用python manage.py show_urls这是一个第三方命令可通过django-extensions获得能列出项目中所有已注册的URL是排查路由问题的利器。5.3 自定义动作Action的URL路径不符合预期问题场景你使用action装饰器定义了一个方法但生成的URL不是你想要的。分析与解决action装饰器默认使用方法名作为URL路径。如果你的方法叫mark_as_read路径就是mark_as_read/。使用url_path参数自定义路径action(detailTrue, methods[post], url_pathread)会生成.../{pk}/read/。使用url_name参数自定义反向解析的名称action(..., url_namebook-mark-read)。特别注意detail参数至关重要。detailTrue生成针对单个对象的URL.../{pk}/action/detailFalse生成针对集合的URL.../action/。如果设错会导致视图方法接收到的参数如pk不符合预期引发错误。5.4 序列化器验证通过但save()失败或数据不对问题场景在CreateAPIView或UpdateAPIView中serializer.is_valid()返回True但调用serializer.save()后数据没有保存或者保存的数据不对。排查思路检查序列化器的create和update方法你可能重写了这两个方法但实现有误。确保它们正确地创建或更新了模型实例。检查模型约束数据库层面可能有unique_together、unique约束或者模型save()方法中有自定义逻辑导致保存失败。查看Django的运行日志或数据库返回的错误信息。检查视图中的perform_create或perform_update在GenericAPIView中serializer.save()之后会调用perform_create(serializer)或perform_update(serializer)。你可能重写了这些方法在其中进行了某些操作如设置额外属性影响了保存过程。class BookCreateView(generics.CreateAPIView): ... def perform_create(self, serializer): # 在保存前可以添加一些逻辑例如设置当前用户为所有者 serializer.save(ownerself.request.user)使用事务如果保存涉及多个相关对象考虑使用transaction.atomic()装饰器包裹视图方法或serializer.save()部分确保数据一致性并便于在出错时回滚。5.5 列表接口ListAPIViewN1查询问题问题场景列表接口返回大量数据时响应速度极慢。使用Django Debug Toolbar检查发现执行了数百甚至上千条SQL查询。根因分析这是经典的ORM N1查询问题。例如在BookSerializer中序列化外键关联的author字段author serializers.CharField(sourceauthor.name)当序列化100本书时会先执行1条查询获取所有书然后为每一本书再执行1条查询去获取作者信息总共101条查询。解决方案使用select_related或prefetch_related优化查询。在视图中覆盖get_queryset方法class BookListView(generics.ListAPIView): serializer_class BookSerializer def get_queryset(self): # 使用select_related优化一对一或外键关系 return Book.objects.all().select_related(author, publisher) # 对于多对多或反向关系使用prefetch_related # return Book.objects.all().prefetch_related(tags, reviews)更精细的控制有时你无法确定视图会如何使用序列化器。一个更稳健的做法是在序列化器内部通过重写__init__或使用第三方库如drf-optimize来动态优化查询。但最简单有效的还是在视图的get_queryset中根据序列化器可能用到的关系提前做好select_related和prefetch_related。我个人在实际项目中的体会是DRF的视图和路由系统是一个“约定大于配置”的典范。初期学习概念时会觉得有点绕但一旦掌握开发效率的提升是巨大的。对于标准的资源型API我的首选组合是ModelViewSetDefaultRouter再辅以action装饰器处理自定义端点。对于非标准或逻辑特别复杂的单个端点则退回到APIView。在配置路由时务必注意basename的规则并善用python manage.py show_urls来验证你的URL配置是否正确生效。最后永远不要忘记性能对列表视图的查询集queryset进行select_related和prefetch_related优化是保证API响应速度的基础功课。
返回列表