Skip to content

资源与资源层次

本章导读:"资源建模"是 REST 设计的第一步,也是最容易走偏的一步。资源不是数据库表,资源层次也不等于外键关系。本章给出一套可操作的资源识别方法与层次设计规则,并用电商/博客案例演示常见陷阱。

1. 资源到底是什么

Fielding 的定义(论文 5.2.1):

"资源是网络信息源上可供识别的任何概念性映射……资源的设计独立于操作资源的系统。"

翻译成工程语言:

  • 资源是业务概念的映射,不是存储实体。同一篇数据库记录,可以映射出"文章资源""文章草稿资源""文章发布版资源"多个 REST 资源。
  • 资源有生命周期与状态,但资源本身是稳定的抽象:/articles/42 永远指向"那篇文章",至于它今天是草稿还是已发布,是资源状态的变化,不是 URL 的变化。
  • 资源可以不是数据:"今天北京的天气""当前登录会话""第 3 页搜索结果"都是合法资源——它们只是服务器生成的一种表现。

2. 识别资源的四问

拿到一个业务需求,依次问:

  1. 它是名词吗? 用户、订单、文章、地址、购物车 → 资源。
  2. 它有独立生命周期吗? 会被单独创建/查询/修改/删除吗?评论可以脱离"某篇文章的上下文"被单独编辑 → 评论是资源,而不只是文章 JSON 里的一个数组字段。
  3. 它有一组吗? "文章集合""用户的订单列表"是集合资源,本身也是一个资源(可 GET、可 POST 创建成员)。
  4. 它是动作还是状态? "发布文章"不是资源;"文章的状态字段"或"一次发布产生的发布记录(publication)"才是资源。把动词转化为名词,是资源建模的核心功。

3. 集合资源与单个资源

REST 对资源只有两种基本形态,接口由这两层展开:

形态URI 模式允许的方法(RFC 9110)典型语义
集合资源/articlesGET(列表)、POST(创建)GET 返回表现列表;POST 追加成员
单个资源/articles/{id}GET、PUT、PATCH、DELETE针对唯一成员的操作
http
GET  /articles            → 200 文章列表(分页表现)
POST /articles            → 201 新建,Location: /articles/43
GET  /articles/42         → 200 文章 42
PUT  /articles/42         → 204 全量替换
DELETE /articles/42       → 204 删除

WARNING

集合上不定义 GET 之外的批量写语义。DELETE /articles(删库跑路)在任何规范里都不是合法的默认行为——批量删除必须走显式设计(见批量操作设计)。

4. 资源层次:从属关系的两种建模

评论从属于文章。URI 里到底要不要体现这层关系?两种方案都合法,语义不同:

4.1 从属型(subordinate)——评论的生命周期完全挂在文章下

GET    /articles/42/comments        ← 集合
POST   /articles/42/comments        ← 创建
GET    /articles/42/comments/7      ← 单资源
PATCH  /articles/42/comments/7
DELETE /articles/42/comments/7

适用:父资源不移动到其它父(评论永远属于文章 42);权限天然沿父资源继承。

4.2 扁平型(flat)——评论有独立身份

GET    /comments/7
PATCH  /comments/7        ← 编辑评论不需要知道它是哪篇文章的
GET    /articles/42/comments?after=...  ← 仅作为"查询入口"存在

适用:资源可能更换父级(文件在目录间移动)、需要在未知父 ID 时直接操作(后台按评论 ID 管理)、跨端只拿到评论链接的场景。

决策规则:

  • 从属深度最多两层/users/7/articles/42/comments/7 是设计失败的信号——改用最内层资源的扁平 URI。
  • 集合入口可以保留从属路径(方便过滤),单资源操作用扁平路径(GitHub 的 /repos/{owner}/{repo}/issues/{n}notifications 里直接给 issue URL 并存即此思路)。

5. 子资源 vs 字段:什么时候切开

文章"附带"作者、分类、封面图、草稿正文,哪些要拆成独立资源?

判据不拆
单独高频读(只看封面)✔ 封面可拆 /articles/42/thumbnail✘ 标题、摘要随主资源返回
单独修改且并发敏感✔ 草稿拆为 /articles/42/draft✘ 状态字段少改动的随主资源
体积巨大/流式✔ 正文可拆 /articles/42/content,附件用对象存储 URL✘ 小字段
权限不同✔ 草稿仅作者可见 → 必须拆

拆与不拆的核心问题永远是:"是否存在只操作它、或只读取它的自然场景?"

6. 资源标识:ID 的选型

/articles/{id} 中的 {id} 用什么?

类型适用与风险
自增数字42简单;可被枚举爬取、泄露业务规模,对外慎用
UUID0f8f3e2a-…无冲突、不可枚举;可读性差、B+ 树索引碎片化(可用有序 UUIDv7 缓解)
雪花 ID1873201933…趋势递增 + 不可预测性折中;暴露时间戳
业务键/users/ma_gua/orders/20260918-001可读、可预期;一旦公开即成契约,不可变更
哈希/短链/articles/bK9xQz对外友好;需保证唯一与防碰撞

规范要点(RFC 3986): ID 若可能包含保留字符(/ ? #)或任意用户输入,必须百分号编码后放入路径,或改用查询参数。例如文件路径型 ID docs/a b.txt/files/docs%2Fa%20b.txt,绝不可直接拼接。

7. 常见陷阱清单

  1. 把表当资源:为提升性能在表现层合并了两张表,URI 结构却照抄表连接——领域模型应独立于存储模型(参见"REST 六大约束"中"通过表现操作资源"子约束)。
  2. 资源名用单数/article/42——社区规范一致推荐集合用复数/articles),全团队统一比选哪个更重要。
  3. 为每个查询建端点/articles/by-author/articles/by-date——过滤是集合资源的查询参数/articles?authorId=7&sort=-createdAt),不是新资源。
  4. 动作 URL 泛滥/articles/42/publish-and-notify——优先转状态/子资源建模;确实无法名词化时才保留动词端点,并在文档标注。
  5. 层次过深:超过两层的从属路径意味着你把导航结构当成了资源结构。

8. 本章小结

  • 资源 = 业务概念的概念性映射,独立于存储;识别资源靠"四问"。
  • 只有两种形态:集合资源与单个资源;方法-形态对应表是 REST 的"语法骨架"。
  • 从属/扁平两种层次建模都合法,按"生命周期与移动性"选择,深度 ≤ 2。
  • 拆分资源的判据:独立读写场景、体积、权限。

9. 下一步

资源定好了,客户端拿到的"表现"长什么样、用什么格式传递 → 表示与媒体类型