- 后端
- API设计
【免费下载链接】django-ninja
💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
本文基于 Django Ninja 官方 Enhancement Proposals(增强提案)文档 docs/docs/proposals/cbv.md 展开,系统解读"Class Based Operations(基于类的操作)"这一设计提案的动机、方案与权衡。文中将提案的核心思想与当前仓库中 Router、Operation、参数解析与异步支持的源码实现进行对照,帮助读者理解:在什么场景下函数式 API 操作会出现重复样板代码,提案如何通过"装饰整个类 + 构造器统一初始化"来化解,以及该方案在异步环境下所面临的语言层面限制。读完本文,读者将掌握这一提案的完整技术细节,并能在实际工程中识别出适合"类化重构"的 API 结构。
一、提案的定位:这是一份设计蓝图,而非已实现功能
首先必须明确一个前提:cbv.md是 Django Ninja 仓库docs/docs/proposals/目录下的一份正式提案文档,文档开头使用了醒目的警告标注:
This is just a proposal and it isnot present in library code, but eventually this can be a part of Django Ninja.
也就是说,文中所描述的@router.path(...)类装饰器语法当前并不存在于库代码中。对仓库源码进行检索可以印证这一点:ninja/router.py中的Router类目前只提供了get、post、delete、patch、put、api_operation、add_api_operation、add_router等装饰器与注册方法,并不存在path方法。因此,本文将其定位为"设计提案 + 源码现状对照"来讲解,读者若要在生产代码中使用类似能力,需要在理解底层机制后自行封装,或等待官方在后续版本中采纳该提案。
提案的产生遵循了 Django Ninja 官方的 Enhancement Proposals 机制(见 docs/docs/proposals/index.md):通过 Pull Request 在docs/docs/proposals/下新增提案页,或通过 issue 发起讨论。cbv.md正是该机制下的第一份提案,其讨论主题是——当多个 API 操作共享大量初始化逻辑(尤其是权限校验)时,能否用"类"来组织代码。
二、Problem:函数式操作中的重复样板代码
提案从一个非常典型的真实场景出发:一个 Todo 应用,包含**项目(Project)与任务(Task)**两个模型:
- 每个项目拥有多个任务;
- 每个项目有一个所有者(用户);
- 用户不能访问不属于自己的项目。
对应 Django 模型结构如下(原文代码,Task中的project外键通过字符串引用Project,需要定义在Project之后或使用字符串引用):
class Project(models.Model): title = models.CharField(max_length=100) owner = models.ForeignKey('auth.User', on_delete=models.CASCADE) class Task(models.Model): project = models.ForeignKey(Project, on_delete=models.CASCADE) title = models.CharField(max_length=100) completed = models.BooleanField()现在要为它实现三个 API 操作:
- 列出某项目下的所有任务;
- 查看某个任务详情;
- 执行"完成任务"动作。
同时,每个操作都必须校验:用户只能访问自己项目下的任务,否则返回 404。用 Django Ninja 当前最标准的"函数式"写法,代码长这样(原文代码):
router = Router() @router.get('/project/{project_id}/tasks/', response=List[TaskOut]) def task_list(request): user_projects = request.user.project_set project = get_object_or_404(user_projects, id=project_id)) return project.task_set.all() @router.get('/project/{project_id}/tasks/{task_id}/', response=TaskOut) def details(request, task_id: int): user_projects = request.user.project_set project = get_object_or_404(user_projects, id=project_id)) user_tasks = project.task_set.all() return get_object_or_404(user_tasks, id=task_id) @router.post('/project/{project_id}/tasks/{task_id}/complete', response=TaskOut) def complete(request, task_id: int): user_projects = request.user.project_set project = get_object_or_404(user_projects, id=project_id)) user_tasks = project.task_set.all() task = get_object_or_404(user_tasks, id=task_id) task.completed = True task.save() return task提案敏锐地指出了其中的问题:这三段代码里,"取出当前用户的项目集合 → 用project_id查找并校验归属 → 继续取任务集合"这几行逻辑被反复复制:
user_projects = request.user.project_set project = get_object_or_404(user_projects, id=project_id))这种重复会带来一系列现实困扰:
- 样板代码膨胀:每新增一个针对"项目下任务"的操作,都要再复制一遍校验代码,即使提取成函数,也只是"少了 3 行",代码依然被污染(原文原话:"You can extract it to a function, but it will just make it 3 lines smaller, and it will still be pretty polluted");
- 出错概率升高:复制粘贴时极易遗漏或改错参数(原文代码中的
project_id在task_list里实际未在函数签名中声明,就是一个值得注意的笔误); - 业务焦点被稀释:真实的业务逻辑(查任务、改任务状态)淹没在权限校验样板中,可读性下降。
三、Solution:用path装饰整个类,让构造器承担公共初始化
提案给出的核心创意是:把"类"本身当作一个 API 路径单元来装饰。不再是"一个函数对应一个 operation",而是"一个类对应一段路径前缀,类的方法对应具体的 HTTP 操作"。
3.1 提案语法全貌
以下是提案中的完整示例(原文代码,request与project_id作为__init__的参数传入):
from ninja import Router router = Router() @router.path('/project/{project_id}/tasks') class Tasks: def __init__(self, request, project_id=int): user_projects = request.user.project_set self.project = get_object_or_404(user_projects, id=project_id)) self.tasks = self.project.task_set.all() @router.get('/', response=List[TaskOut]) def task_list(self, request): return self.tasks @router.get('/{task_id}/', response=TaskOut) def details(self, request, task_id: int): return get_object_or_404(self.tasks, id=task_id) @router.post('/{task_id}/complete', response=TaskOut) def complete(self, request, task_id: int): task = get_object_or_404(self.tasks, id=task_id) task.completed = True task.save() return task这一设计的精妙之处在于路径的拼接与状态的共享:
- 类级装饰器
@router.path('/project/{project_id}/tasks')声明了路径前缀,并把{project_id}这样的路径参数暴露给__init__; - 类内每个方法的装饰器路径(
'/'、'/{task_id}/'、'/{task_id}/complete')会自动与类级前缀拼接,形成完整的 operation 路径; - 三个操作等价于函数式版本中的三个路由,但共享同一个
self.tasks状态。
3.2 构造器承担"公共初始化"
提案把方案的核心高亮放在__init__上:
@router.path('/project/{project_id}/tasks') class Tasks: def __init__(self, request, project_id=int): user_projects = request.user.project_set self.project = get_object_or_404(user_projects, id=project_id)) self.tasks = self.project.task_set.all()所有公共的初始化与权限校验逻辑都收拢进构造器:
request由框架注入,业务代码无需再手工传参;project_id由路径参数注入,框架负责从 URL 中解析并做类型转换;- 校验失败(项目不存在或不属于当前用户)时,
get_object_or_404直接抛出 404,构造过程即中止,后续方法根本不会执行——这相当于把"前置守卫"提升到了类实例化的层面; - 校验通过后,
self.project、self.tasks成为实例属性,各个业务方法只需直接消费它们。
提案指出,这样重构后,"主业务操作只专注于任务本身(通过self.tasks属性暴露)",权限与初始化逻辑完全从各个方法体中剥离。此外提案还明确说明:api实例与router实例都应当支持类路径("You can use bothapiandrouterinstances to support class paths"),即@api.path(...)与@router.path(...)语义一致。
3.3 提案方案的收益对比
| 维度 | 函数式写法 | 类式(提案)写法 |
|---|---|---|
| 权限校验代码 | 每个函数各写一份 | 仅__init__一处 |
| 业务方法参数 | 每个函数独立声明project_id、task_id | 公共路径参数进__init__,方法只声明自身所需参数 |
| 共享状态 | 每次重复查询 | self.tasks缓存复用 |
| 新增操作成本 | 复制校验样板 | 新增一个方法即可 |
四、源码对照:提案语法与 Django Ninja 现有机制的衔接点
虽然@router.path尚未实现,但提案所依赖的底层能力在仓库中均已存在,这保证了方案在架构上的可行性。理解这些衔接点,也有助于读者在现有框架内模拟类似模式。
4.1 路径前缀拼接:Router 已有类似机制
提案要求"类级路径前缀 + 方法级路径"自动拼接。仓库中Router.add_router(prefix, router, ...)(见 ninja/router.py)已经实现了"前缀 + 子路由"的拼接语义:将子 Router 挂载到父 Router 时,prefix会作为路径前缀。Router.urls_paths与build_routers共同完成这种层级化路径的组合。因此,"路径前缀合并"在框架内并非全新概念,@router.path可以视作把同一层级的"前缀合并"能力延伸到类内部。
4.2 操作注册管线:类方法可直接复用现有管线
提案中每个方法仍然使用@router.get(...)、@router.post(...)装饰,这与当前库完全一致。在现有实现中,这些装饰器最终都汇聚到Router.api_operation→Router.add_api_operation(见 ninja/router.py),后者为每个路径维护一个PathView,并按 HTTP 方法追加Operation。换句话说,只要在类实例化后把绑定方法(bound method)当作view_func传入现有的add_api_operation管线,即可复用全部现有能力——包括response响应模型、auth、throttle、tags、summary、openapi_extra等全部装饰器参数。
4.3 参数解析:__init__需要新的注入通道
当前Operation的参数解析基于函数签名:Operation.__init__中通过ViewSignature(path, view_func)(见 ninja/operation.py)解析view_func的签名并生成参数模型,请求到达时由Operation._get_values统一解析出路径参数、查询参数、请求体等(见 ninja/operation.py 的执行链:_run_checks→_get_values→view_func(request, **values))。
提案的类式写法中,__init__也要接收request和路径参数(如project_id),这意味着框架需要先解析出__init__的参数值、实例化类、再调用业务方法。从现有结构看,可以推断有两种可行的落地路径:
- 让
__init__走与view_func相同的签名解析与参数注入逻辑(即把__init__视为一个"前置 operation"); - 或者将"类实例化 + 方法调用"包装成一个合成函数,交给现有
Operation管线统一处理。
两种路径都不需要改动参数解析的核心机制,属于对现有管线的扩展而非重写。此外,提案中def __init__(self, request, project_id=int)这种用默认值int表达类型的写法(而非project_id: int注解),与 Django Ninja 基于类型注解(typing annotations)的参数解析体系并不兼容——可以推断,若该提案落地,__init__的路径参数应当改为标准注解形式project_id: int才能被ViewSignature正确识别。
4.4 认证/权限:与request.user的协作不变
提案中的权限校验依赖request.user.project_set,这与 Django Ninja 现有的认证体系完全兼容:Operation._run_authentication在调用业务函数之前执行认证回调,并把认证结果写入request.auth(见 ninja/operation.py);request.user则来自 Django 自身的中间件与django.contrib.auth。类式方案中,认证逻辑发生在__init__之前(由框架统一处理),__init__内只做"基于已认证用户的授权校验",分层清晰,与现有架构不冲突。
五、Issue:async与__init__的语言层面矛盾
提案在最后坦诚地列出了一个关键设计难题,原文如下:
The
__init__method:def __init__(self, request, project_id=int):— Python doesn't support theasynckeyword for__init__, so to support async operations we need some other method for initialization, but__init__sounds the most logical.
这是提案中最具讨论价值的部分:
- 问题本质:
__init__是同步的构造方法,Python 语言规范不允许async def __init__。如果类的构造过程中包含异步操作(例如在异步环境下await查询数据库、调用外部 API),就无法把初始化逻辑放进__init__; - 两难处境:
__init__在语义上是最自然的"初始化"位置(提案原文:"__init__sounds the most logical"),但异步场景又需要一种替代的初始化通道; - 潜在替代方向:从提案上下文可以推断几种候选方案——显式的异步工厂方法(如
async def create(...)类方法)、独立的async def init(...)钩子(由框架在调用业务方法前自动await)、或把异步初始化放在第一个业务方法内部惰性完成。提案没有给出定论,而是将选择权交给社区讨论。
5.1 结合仓库现状:Django Ninja 的异步能力边界
Django Ninja 对异步操作的支持已经相当成熟:框架通过is_async(view_func)检测函数是否为协程,并据此选择Operation或AsyncOperation(见 ninja/operation.py 中PathView.add_operation的分支逻辑);AsyncOperation.run使用async def执行完整的_run_checks→_get_values→view_func流程(见 ninja/operation.py)。异步相关的最佳实践可参考 docs/docs/guides/async-support.md。
对照这一现状,类式提案的异步短板就更加突出:
- 函数式异步操作只需在函数前加
async关键字即可(见 docs/docs/guides/async-support.md 中async def say_after(...)的示例); - 而类式方案中,即使业务方法可以写成
async def,构造阶段的异步化仍受__init__限制——这正是提案公开征求社区意见的核心点。
5.2 一个值得注意的细节:同步操作内做 ORM 查询没有障碍
值得注意的是,提案示例中的__init__执行的是同步 ORM 查询(get_object_or_404、.all()),这在 Django Ninja 现有的同步Operation.run执行链中不存在任何障碍:_get_values解析参数后同步调用view_func(request, **values),类实例化同样同步发生。因此,对于纯同步的 API,类式提案的实现路径是清晰可行的;真正的设计争议集中在异步场景的初始化通道上。
六、结语:提案的价值与落地展望
cbv.md作为 Django Ninja Enhancement Proposals 体系下的首份提案,其价值至少体现在三个层面:
- 问题识别精准:它指出的"跨操作重复初始化/授权样板"在真实工程中极其常见,尤其是"嵌套资源 + 属主校验"这类 CRUD 场景;
- 方案简洁优雅:仅通过"装饰类 + 构造器注入 + 路径拼接"三个概念,就实现了对函数式样板代码的大幅压缩,且与现有 Router/Operation 管线高度兼容;
- 权衡讨论坦诚:它没有回避
async __init__这一硬约束,而是将其明确列为待社区决策的开放问题,体现了提案机制的严谨性。
对于希望立即在现有 Django Ninja 项目中缓解同类问题的读者,在提案落地之前,可以结合仓库现有能力采取替代策略:例如用 docs/docs/guides/routers.md 中的 Router 层级组织嵌套资源,将公共授权逻辑提取为可复用的认证类(参考 docs/docs/guides/authentication.md),或利用装饰器组合(ninja/decorators.py中的decorate_view)封装通用前置逻辑。这些手段虽然不及"类装饰"来得彻底,但同样是消除重复样板的有效路径,且完全基于当前库的稳定 API。
最后回到提案本身:它是一份待讨论的设计蓝图,而非可直接使用的功能。感兴趣的读者可以在仓库的 docs/docs/proposals/index.md 查看提案机制的说明,并关注cbv.md的后续演进——如果该提案被采纳,Django Ninja 将同时具备函数式与类式两种操作组织方式,覆盖从轻量单函数到重度嵌套资源的不同工程需求。
- 后端
- API设计
【免费下载链接】django-ninja
💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
相关推荐
解锁kiosk模式:chromium_os-raspberry_pi专属功能配置与应用场景实战
解锁kiosk模式:chromium_os raspberry_pi专属功能配置与应用场景实战 chromium_os raspberry_pi是一款专为树莓派
ZXing代码复用度量:识别并消除重复代码
ZXing代码复用度量:识别并消除重复代码 引言:代码复用的重要性 在软件开发过程中,代码复用是提高效率、降低维护成本的关键实践。ZXing(Zebra Cro
图像处理计算机视觉Czkawka 磁盘清理指南:14 个免费工具快速找回被重复文件占用的空间
Czkawka 磁盘清理指南:14 个免费工具快速找回被重复文件占用的空间 打开磁盘属性,1TB 的硬盘只剩 4GB,却不知道空间都被谁吃掉了。Czkawka
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考