ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django Rest Framework 实战:从项目设计到性能优化的完整指南

Django Rest Framework 实战:从项目设计到性能优化的完整指南 Django Rest FrameworkDRF这块我从第一次在 Django 项目里硬怼 API 开始就一直在用陆陆续续踩了不少坑也沉淀了一套自己比较顺手的设计流程。这篇文章就以“使用 Django Rest Framework 构建 API”为切入点把从项目设计、环境搭建、核心代码实现到线上踩坑排查的完整经验一次讲清楚。内容偏向实战更适合已经跑通 Django 基础、准备正式上手 API 开发的读者当然零基础的同学跟着一步步来也能跑起来我会尽量把每个环节的“为什么这么做”也说明白。1. 内容整体设计与思路拆解1.1 为什么是 DRF而不是普通 Django 视图或 Flask很多第一次写接口的人会问我Django 本身用 JsonResponse 就能返回 JSON为什么还要引入一个 DRF我的回答一般很直接如果只是做一两个简单的接口确实用不着 DRF但当你需要面对序列化、反序列化、参数校验、认证权限、分页、限流、文档生成这一堆问题时DRF 的价值就完全体现出来了。我自己最早用JsonResponse写接口时的感受是每个视图都要手动处理 query 参数、request.body 解析、字段缺失检查、类型转换、错误返回一套下来模板代码一大堆而且不同接口之间的风格很难统一。DRF 把这块抽象成了“序列化器 视图 路由器”的组合开发效率提升特别明显尤其是字段校验和嵌套关系处理省掉了大量重复劳动。和 Flask 加 SQLAlchemy 的方案对比DRF 最大的优势是和 Django ORM、Admin、迁移体系深度绑定。你不用额外去定义一套 schema直接在 Model 基础上派生 ModelSerializer 就可以。团队协作时发现字段模型变了改一下 Meta 配置API 文档、表单校验、前后端契约全部同步这种一致性在项目迭代中特别有价值。当然 Flask 更轻适合微服务或小工具但如果你已经选了 Django 作为业务主体API 层还是老老实实用 DRF 最划算。1.2 一套合理 API 的基础设计约定动工之前先把设计约定定好后面实现会舒服很多。分享下我常用的约定都是踩过不少坑之后总结出来的不一定所有项目都适用但可以当个起点。接口路径采用资源型设计比如/api/users/、/api/orders/集合操作用复数名词单个资源操作后面跟 ID/api/users/1/。DRF 默认要末尾斜杠路由层会统一处理保持团队习惯一致就行。动作型操作尽量收敛到资源子路径比如订单支付定义POST /api/orders/{id}/pay/而不是散落成/api/pay_order/、/api/query_pay_status/这种扁平 URL。这样后面做权限控制、文档分组、日志统计都会简单很多。版本管理用 URL 前缀比如/api/v1/不推荐用 header 或参数传版本号URL 中带版本最直观排查问题也方便客户端升级成本最低。响应结构统一包一层{code: 0, message: ok, data: ...}也行但要注意别把它做成全局中间件用 DRF 的异常处理类和 renderer 统一处理更可控。不过如果项目主要给自家前端用直接用 DRF 默认的响应格式也足够过度包装反而会增加前后端联调负担。设计时多花半小时把资源、关联、权限边界梳理清楚后面整个开发周期的返工量都会明显下降。2. 核心细节解析与实操要点2.1 序列化器是 DRF 的心脏别只当它是“序列化工具”很多初学 DRF 的朋友以为 Serializer 只是把 Model 转成 JSON实际上它的反序列化能力才是主要发力点。简单说前端传进来一份 JSONSerializer 负责校验字段、清洗数据、转换格式然后才交给视图层创建或更新实例。我们在项目里把 Serializer 当成“入站管线和出站模型的统一网关”所有数据边界都在这里控制而不是散落到视图逻辑里。举个例子用户注册接口通常需要确认密码字段但这个字段不应该存到数据库里。在 ModelSerializer 中可以通过extra_kwargs设置write_onlyTrue序列化时不会输出反序列化时又能接收。再比如日期字段前端传2025-09-10 15:30:00这种字符串Serializer 的 DateTimeField 会自动转成 datetime 对象再进入模型层不需要手动解析。嵌套序列化器也是高频需求但使用时要克制。嵌套太深会导致序列化性能骤降外层查 100 条订单、每条查 20 个关联商品普通写法就是 100 次额外查询。这个问题我会在后面的性能小节展开。正确做法是展示层用嵌套序列化器列表页尽量用SerializerMethodField配合预取查询或者针对列表场景单独定义精简的 ReadOnly 序列化器。2.2 视图层的三种写法该怎么选DRF 的视图有函数视图api_view、APIView 基类和 ViewSet 三种形态我见过团队三种混用导致代码风格很乱的所以给定了个选型套路。最简单、最直白的场景比如一个健康检查接口、一个回调通知接口用api_view([GET])就够了读起来一目了然。常规资源型接口比如用户列表、详情、创建、更新用ModelViewSet效率最高它把 List、Retrieve、Create、Update、Destroy 全部内置配上一个 Router 就能注册完所有路由。如果是“一个视图只负责一个动作”的逻辑比如某个上报接口用APIView子类更清晰不用被迫继承 ViewSet 那套 mixin。我用得最多的还是ModelViewSet但不是闭着眼睛什么资源都往 ModelViewSet 上放。复杂业务里我会优先拆成“读操作走 ViewSet 自带的 List/Retrieve写操作走自定义 action”。比如订单模块查看有各种状态查询、分页、排序需求直接用 ModelViewSet 重写get_queryset反而容易失控我会单独定义一个只读 ViewSet 加上几个 action代码结构更稳。2.3 认证、权限与限流的正确配置姿势DRF 默认的配置很保守SessionAuthentication、AllowAny 权限这在生产环境基本不可用。我的习惯是最少配两层认证Session 认证给 Admin 后台场景JWT 认证给移动端和第三方客户端。JWT 我最初用djangorestframework-simplejwt比较多它有现成的TokenObtainPairView和刷新 token 接口接入成本很低。权限配置要注意别只依赖全局默认具体 ViewSet 里要针对 action 单独配置。比如普通用户可以查看自己的个人信息但不能查看别人管理员才能删除用户。这种需求可以重写get_permissions方法在列表、详情、自定义动作之间做区分。限流是 API 上线后的保命配置我常配三类匿名用户按 IP 限流登录用户按用户限流高成本接口再单独加大限制。DRF 的AnonRateThrottle和UserRateThrottle组合使用就能覆盖大部分场景。限流不是防止恶意攻击的银弹但在接口被误刷或爬虫盯上时它可以给运维争取到足够的应对时间。3. 实操过程与核心环节实现3.1 初始化项目与依赖安装先说下环境我这边的实践是基于 Django 5.0 和 Django REST Framework 3.15 版本Python 使用 3.10 以上。新项目建议直接用虚拟环境避免系统 Python 环境被弄乱。mkdir blog_api cd blog_api python -m venv venv source venv/bin/activate pip install django djangorestframework # 如果你要用 JWT、过滤器、接口文档再装这几个 pip install djangorestframework-simplejwt django-filter drf-spectacular打开settings.py把rest_framework加入INSTALLED_APPS再加一行默认配置REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework.authentication.SessionAuthentication, rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticatedOrReadOnly, ], DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20, DEFAULT_FILTER_BACKENDS: [ django_filters.rest_framework.DjangoFilterBackend, rest_framework.filters.SearchFilter, rest_framework.filters.OrderingFilter, ], }我加了PAGE_SIZE 20这个值别设太大否则前端一次拿 200 条数据很臃肿也容易触发数据库压力。搜索与排序的默认后端开了以后列表接口直接支持?search和?ordering参数很贴心。注意rest_framework_simplejwt的 JWT 认证和 Django 自带的 SessionAuthentication 都加进去没有任何冲突JWT 用来处理接口鉴权Session 方便你后面在本地浏览器调试 DRF 自带的可视化页面。3.2 定义模型与迁移我以一个简单的博客系统为例包含作者、文章和评论三个模型。我把自定义用户模型放在第一步做因为一旦你完成了第一次迁移想再把默认的auth.User替换成自定义用户就非常麻烦了。from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): nickname models.CharField(max_length32, blankTrue) bio models.TextField(blankTrue) class Article(models.Model): author models.ForeignKey(User, on_deletemodels.CASCADE, related_namearticles) title models.CharField(max_length200) content models.TextField() created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) is_published models.BooleanField(defaultFalse) class Comment(models.Model): article models.ForeignKey(Article, on_deletemodels.CASCADE, related_namecomments) user models.ForeignKey(User, on_deletemodels.CASCADE, related_namecomments) body models.TextField() created_at models.DateTimeField(auto_now_addTrue)在settings.py里配置AUTH_USER_MODEL blog.User然后执行python manage.py makemigrations python manage.py migrate自定义用户模型这件事我有切身体会项目上线后再想从auth.User换成AbstractUser子类需要写复杂的数据迁移甚至要重建一堆外键成本极高。只要项目还没上线一律先定义好自定义用户模型。3.3 编写序列化器ViewSet 的代码非常薄业务的绝大部分都沉淀在序列化器里。以下是一个 ArticleSerializer 的中等复杂度示例from rest_framework import serializers from .models import Article, Comment from django.contrib.auth import get_user_model User get_user_model() class UserSerializer(serializers.ModelSerializer): class Meta: model User fields [id, username, nickname, bio] class CommentSerializer(serializers.ModelSerializer): user UserSerializer(read_onlyTrue) class Meta: model Comment fields [id, user, body, created_at] class ArticleSerializer(serializers.ModelSerializer): author UserSerializer(read_onlyTrue) comments CommentSerializer(manyTrue, read_onlyTrue) comment_count serializers.IntegerField(sourcecomments.count, read_onlyTrue) class Meta: model Article fields [ id, title, content, author, comments, comment_count, created_at, updated_at, is_published, ] read_only_fields [created_at, updated_at] def validate_title(self, value): if len(value.strip()) 5: raise serializers.ValidationError(标题不能少于5个字符) return value.strip()这里我特别说明一下comment_count用sourcecomments.count虽然写起来方便但会产生一个额外聚合查询如果你的列表页有几千条数据建议在视图层用annotate一次性算好再传给序列化器性能和可维护性都更好。validate_title是字段级校验钩子还可以定义validate(self, attrs)做对象级关联校验。序列化器把校验逻辑放在一起测试时可以直接单测 Serializer不用去发 HTTP 请求这个设计效率提升非常明显。3.4 编写视图与路由有了序列化器视图层代码就非常简单了。我按前面说的选型规则只读列表和详情用 ReadOnlyModelViewSet写操作拆成 action。from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response from django_filters.rest_framework import DjangoFilterBackend from .models import Article from .serializers import ArticleSerializer class ArticleViewSet(viewsets.ReadOnlyModelViewSet): 对外只开放读接口写操作通过 action 控制。 queryset Article.objects.filter(is_publishedTrue).select_related(author).prefetch_related(comments) serializer_class ArticleSerializer filter_backends [DjangoFilterBackend, viewsets.filters.SearchFilter, viewsets.filters.OrderingFilter] filterset_fields [is_published, author] search_fields [title, content] ordering_fields [created_at, updated_at] action(detailTrue, methods[post], permission_classes[permissions.IsAuthenticated]) def publish(self, request, pkNone): article self.get_object() article.is_published True article.save() return Response(statusstatus.HTTP_200_OK)这里queryset我用select_related(author)和prefetch_related(comments)把最常用的关联查询提前加载列表接口的性能和未优化版本有非常明显的差距。尤其注意prefetch_related(comments)之后对 comments 再取countDjango 会自动利用预取缓存不会产生大量重复 SQL。路由注册用 DRF 的 DefaultRouter 即可from rest_framework.routers import DefaultRouter from django.urls import path, include from .views import ArticleViewSet router DefaultRouter() router.register(rarticles, ArticleViewSet) urlpatterns [ path(api/, include(router.urls)), ]设置router.register(rarticles, ArticleViewSet)之后列表、详情和publish自定义动作都自动映射好了路径分别是/api/articles/、/api/articles/{id}/、/api/articles/{id}/publish/。Router 虽省事但要注意它生成的 URL 规则比较固定如果前端对 URL 格式有特殊要求还是手动写路由更灵活。3.5 配置 JWT 认证与接口文档JWT 的接入很简单在根路由里加上from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns [ path(api/token/, TokenObtainPairView.as_view()), path(api/token/refresh/, TokenRefreshView.as_view()), ]拿到 token 之后前端请求头加Authorization: Bearer token即可。SimpleJWT 默认的 access token 有效期比较短通常设置 5-30 分钟refresh token 有效期数天这样设计是为了降低 token 泄露的风险。接口文档我用 drf-spectacular主要是因为对 DRF 的 schema 支持最好OpenAPI 3 标准也主流INSTALLED_APPS [drf_spectacular] SPECTACULAR_SETTINGS { TITLE: Blog API, VERSION: v1, }注册路由from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns [ path(api/schema/, SpectacularAPIView.as_view(), nameschema), path(api/docs/, SpectacularSwaggerView.as_view(url_nameschema), nameswagger-ui), ]有自动文档之后前后端联调、测试同学编写用例、给外部合作伙伴提供接口说明都会省很多沟通成本。我见过的很多团队接口文档靠维护一个 Word 或在线表格版本一迭代就没人更新了最后还是要跑去看代码OpenAPI 文档因为是代码自动生成的更新频率和代码保持同步至少不会太离谱。4. 常见问题与排查技巧实录4.1 N1 查询爆炸问题这是目前我接手过的 DRF 项目里最常见的性能问题。一个订单列表接口前端只显示订单和商品名但接口用了嵌套序列化器结果每行订单都额外查询一次商品表。数据量到几千条时接口响应从几十毫秒直接飙到几秒。解决核心就一句话视图的queryset必须主动预取。给一段代码参考queryset Article.objects.select_related(author).prefetch_related(author__articles, comments)Django 的预取机制不只能跨一层用双下划线可以预取关联的关联。如果你的业务里还要对关联对象做条件过滤比如只统计文章下面的有效评论建议用Prefetch对象from django.db.models import Prefetch valid_comments Comment.objects.filter(is_approvedTrue) queryset Article.objects.prefetch_related(Prefetch(comments, querysetvalid_comments))排查方法也很简单本地开发时加个 Django Debug Toolbar看一眼 SQL 数量就明白了。上线之后可以用django-silk做接口级 profiling它会记录每个接口的查询次数和耗时定位慢接口的根因很方便。4.2 CSRF 校验导致 POST 接口失败使用 SessionAuthentication 时如果前端没有携带 CSRF tokenPOST 请求会返回 403。我最早遇到过好多次类似CSRF Failed: CSRF token missing or incorrect的错误。原因是我用 SessionAuthentication却没有在请求头中加入X-CSRFToken。DRF 的官方建议是如果完全无浏览器场景可以只保留 TokenAuthentication 或 JWT 认证不使用 SessionAuthentication。但如果你要在浏览器里调 DRF 自带的可视化页面就会发现纯 JWT 认证会让这个页面无法登录。我的解决方案是同时保留两个认证类前端请求都走 JWTCSRF 不生效本地调试时用 Session 登录浏览器页面就能直接操作数据。这个配置在生产环境经历过多轮迭代稳定可靠。如果你就是要在 Session 模式下给纯接口端使用那就在请求 header 里显式加上 CSRF token。手动去模板里抓csrftokencookie 再放 header 的体验确实很繁琐所以我更推荐默认走 JWT。4.3 自定义用户模型引发的序列化器坑你定义User(AbstractUser)之后如果序列化器里用了sourceuser.username或者直接引用 Django 默认的 User容易在创建文章、评论时报Cannot assign ... must be a User instance之类的错误。原因是你在不同地方混用了get_user_model()返回的自定义 User 和原始django.contrib.auth.models.User。经验是项目里所有涉及用户模型的地方都统一用settings.AUTH_USER_MODEL或get_user_model()不要直接from django.contrib.auth.models import User导入。写模型的 ForeignKey 时官方推荐用settings.AUTH_USER_MODEL字符串引用这样 Django 在内部做依赖解析时会自动指向自定义模型避免迁移时生成错误的表名和依赖。4.4 分页和筛选组合使用的小坑很多人一开始会把DEFAULT_PAGINATION_CLASS配成分页然后在某几个 ViewSet 里又自定义了分页类结果发现接口不生效。DRF 的配置优先级是视图类中定义的pagination_class settings 里的DEFAULT_PAGINATION_CLASS。所以如果你只想某个接口用不同的分页在对应 ViewSet 里重写pagination_class即可不要试图去动全局配置。筛选参数在分页模式下要注意一个习惯问题?page1searchpythonordering-created_at这种 URLDRF 的 SearchFilter 会在get_queryset阶段执行分页在之后执行所以翻页时并不会丢失筛选条件。但如果自己在get_queryset里重写了过滤逻辑就一定要处理request.query_params否则翻页后筛选条件就失效了。4.5 接口突然 500日志却没有关键错误信息这个是 DRF 比较经典的一个问题数据库出现唯一约束冲突、外键完整性错误时Django 默认会返回 500但日志信息有时候只显示一个IntegrityError没有堆栈。你很难判断是哪个字段冲突。经验做法是在视图层对外部输入做更严格的校验尤其是唯一字段。比如用户名注册Serializer 的validate_username里直接查一下是否存在虽然多一条 SQL但能给出友好的提示信息。另一种做法是自定义 DRF 的 exception handler对IntegrityError统一转换成 400并把数据库错误信息剥离出用户可理解的文案。5. 从纯 CRUD 到真实业务场景的进阶建议5.1 事务与原子操作DRF 自带的方法比如create、update本身就是在一个事务里执行的但如果你在一个 action 里手动做了多步写操作比如扣库存、创建订单、发送消息就要显式开启事务。from django.db import transaction action(detailTrue, methods[post], permission_classes[permissions.IsAuthenticated]) def order(self, request, pkNone): article self.get_object() with transaction.atomic(): # 扣库存 # 创建订单 # 更新商品状态 pass return Response(statusstatus.HTTP_201_CREATED)transaction.atomic块内部出现异常时自动回滚避免半提交状态。这里我踩过的坑是把对外部服务的调用也放进了事务内部比如下单后调用第三方支付接口结果第三方接口响应超时事务被一直挂住数据库连接被长时间占用。正确做法是将外部调用放在事务提交之后使用事务回调transaction.on_commit来触发外部请求这个设计初看会麻烦一点但线上稳定性会好很多。5.2 异步任务和 DRF 的边界如果一个 API 请求内部逻辑超过几百毫秒比如生成报告、批量发邮件、调用大模型接口它不适合在请求进程里同步执行。这时候我通常采取折中方案接口只做参数校验和任务入库真正执行用异步队列 worker 去处理前端通过另一个状态查询接口轮询结果。DRF 本身不限制你使用async_to_sync还是Celery关键设计思路是让 API 层尽量保持“轻快”重逻辑全部下沉到 worker。我看项目里最常见的问题是开发图方便把耗时逻辑写在 ViewSet 里最后 QPS 稍微上去worker 数量不足请求直接超时。设计接口时把耗时操作的异步化作为默认方案后面再加并发负载会从容很多。5.3 测试策略DRF 的 API 测试我有一套固定写法用APITestCase提供 client对象创建走setUpTestData批量初始化重点覆盖权限控制、序列化校验、分页边界这三块。from rest_framework.test import APITestCase from django.contrib.auth import get_user_model from .models import Article User get_user_model() class ArticleApiTestCase(APITestCase): classmethod def setUpTestData(cls): cls.user User.objects.create_user(usernamealice, passwordtestpass123) Article.objects.create(authorcls.user, title测试标题, content内容) def test_article_list_returns_paginated(self): response self.client.get(/api/articles/) self.assertEqual(response.status_code, 200) self.assertIn(results, response.data)测试里最容易忽略的是权限用例。比如未登录用户不能访问某个接口一定写一条反向用例只预期 POST 成功的接口除了正向用例还要测试缺字段、类型错误、长度超限等场景。DRF 测试写起来不算复杂但保护效果很值重构序列化器或者调整查询逻辑时心里有底。最后分享一点我的实操心得用了这么长时间 DRF我最大的体会是这个框架把大部分重复工作都抽象好了真正的难点反而在于“设计”——怎么规划资源边界、怎么控制查询复杂度、怎么搞定权限与数据隔离。它不像某些前后端不分的项目那样把接口逻辑埋在视图里DRF 强制你按照 Serializer – View – Router 的层次思考。虽然上手初期会有点绕不习惯的人甚至会嫌弃它“太重”但等你维护过几个月、需求迭代过几轮就会明白这种分层带来的稳定性。如果你正准备把一个纯 Django 项目改造成前后端分离架构或者新开一个 API 主导的服务我真心建议你把 DRF 的官方文档通读一遍再照着我的实操经验把最小可用模型搭起来。做出来的第一版可能不算完美但一定比从零开始手写接口顺畅得多。
返回列表