Appearance
资源与资源层次
本章导读:"资源建模"是 REST 设计的第一步,也是最容易走偏的一步。资源不是数据库表,资源层次也不等于外键关系。本章给出一套可操作的资源识别方法与层次设计规则,并用电商/博客案例演示常见陷阱。
1. 资源到底是什么
Fielding 的定义(论文 5.2.1):
"资源是网络信息源上可供识别的任何概念性映射……资源的设计独立于操作资源的系统。"
翻译成工程语言:
- 资源是业务概念的映射,不是存储实体。同一篇数据库记录,可以映射出"文章资源""文章草稿资源""文章发布版资源"多个 REST 资源。
- 资源有生命周期与状态,但资源本身是稳定的抽象:
/articles/42永远指向"那篇文章",至于它今天是草稿还是已发布,是资源状态的变化,不是 URL 的变化。 - 资源可以不是数据:"今天北京的天气""当前登录会话""第 3 页搜索结果"都是合法资源——它们只是服务器生成的一种表现。
2. 识别资源的四问
拿到一个业务需求,依次问:
- 它是名词吗? 用户、订单、文章、地址、购物车 → 资源。
- 它有独立生命周期吗? 会被单独创建/查询/修改/删除吗?评论可以脱离"某篇文章的上下文"被单独编辑 → 评论是资源,而不只是文章 JSON 里的一个数组字段。
- 它有一组吗? "文章集合""用户的订单列表"是集合资源,本身也是一个资源(可 GET、可 POST 创建成员)。
- 它是动作还是状态? "发布文章"不是资源;"文章的状态字段"或"一次发布产生的发布记录(publication)"才是资源。把动词转化为名词,是资源建模的核心功。
3. 集合资源与单个资源
REST 对资源只有两种基本形态,接口由这两层展开:
| 形态 | URI 模式 | 允许的方法(RFC 9110) | 典型语义 |
|---|---|---|---|
| 集合资源 | /articles | GET(列表)、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 | 简单;可被枚举爬取、泄露业务规模,对外慎用 |
| UUID | 0f8f3e2a-… | 无冲突、不可枚举;可读性差、B+ 树索引碎片化(可用有序 UUIDv7 缓解) |
| 雪花 ID | 1873201933… | 趋势递增 + 不可预测性折中;暴露时间戳 |
| 业务键 | /users/ma_gua、/orders/20260918-001 | 可读、可预期;一旦公开即成契约,不可变更 |
| 哈希/短链 | /articles/bK9xQz | 对外友好;需保证唯一与防碰撞 |
规范要点(RFC 3986): ID 若可能包含保留字符(/ ? #)或任意用户输入,必须百分号编码后放入路径,或改用查询参数。例如文件路径型 ID docs/a b.txt → /files/docs%2Fa%20b.txt,绝不可直接拼接。
7. 常见陷阱清单
- 把表当资源:为提升性能在表现层合并了两张表,URI 结构却照抄表连接——领域模型应独立于存储模型(参见"REST 六大约束"中"通过表现操作资源"子约束)。
- 资源名用单数:
/article/42——社区规范一致推荐集合用复数(/articles),全团队统一比选哪个更重要。 - 为每个查询建端点:
/articles/by-author、/articles/by-date——过滤是集合资源的查询参数(/articles?authorId=7&sort=-createdAt),不是新资源。 - 动作 URL 泛滥:
/articles/42/publish-and-notify——优先转状态/子资源建模;确实无法名词化时才保留动词端点,并在文档标注。 - 层次过深:超过两层的从属路径意味着你把导航结构当成了资源结构。
8. 本章小结
- 资源 = 业务概念的概念性映射,独立于存储;识别资源靠"四问"。
- 只有两种形态:集合资源与单个资源;方法-形态对应表是 REST 的"语法骨架"。
- 从属/扁平两种层次建模都合法,按"生命周期与移动性"选择,深度 ≤ 2。
- 拆分资源的判据:独立读写场景、体积、权限。
9. 下一步
资源定好了,客户端拿到的"表现"长什么样、用什么格式传递 → 表示与媒体类型