← [ BACK_TO_NOTES ]

GraphQL Normalized Cache

Normalized Cache 把 GraphQL 查询结果按实体身份拆分存储,使同一实体在多个查询中共享状态,并解释 Mutation 响应、列表关系与实时事件如何影响 Apollo 缓存一致性。

#concept #growing #frontend #graphql #react

[!info] related notes

GraphQL Normalized Cache

一句话定义

Normalized Cache 不把每个 Query 的整棵 JSON 当成孤立副本,而是把可识别实体拆出来,以稳定 identity 保存,再让各 Query 引用这些实体。

为什么需要它

假设两个 Query 都返回同一个用户:

query {
  user(id: "1") { id name }
}

以及:

query {
  diagnosis(id: "d1") {
    id
    owner { id name }
  }
}

如果按“完整响应”缓存,就可能存在两份:

Query A → { User 1 }
Query B → { Diagnosis d1 → User 1 }

当 User 1.name 更新时,两份副本可能失去一致性。

Normalized Cache 更像:

User:1
  name = Sean

Diagnosis:d1
  owner → User:1

两个 Query 都引用同一个实体。

Entity Identity

Apollo 常根据:

__typename + id

形成实体身份,例如:

User:1
Diagnosis:d1

所以稳定 id 非常重要。

没有可靠 identity 的对象更难被独立 normalize,可能只能嵌在父对象里处理。

Mutation 为什么有时会“自动更新”

假设缓存已有:

User:1 { name: "Sean" }

Mutation 返回:

{
  "__typename": "User",
  "id": "1",
  "name": "Arlon"
}

客户端识别出这是同一个 User:1,就可以合并:

User:1 { name: "Arlon" }

引用该实体的视图也能一起更新。

核心来源是 Mutation Response,不是单纯 Mutation Input

不要把自动更新理解成:

updateUser(id=1, name=Arlon)
→ Apollo 看到参数
→ 自动相信缓存应该变成 Arlon

更准确的是:

Mutation input
→ Server 执行业务逻辑
→ Mutation response 返回最终实体
→ Apollo 根据 entity identity normalize / merge

因为服务器可能:

  • 拒绝输入;
  • 修正或规范化输入;
  • 同时修改其他字段;
  • 触发业务副作用。

客户端不能默认把输入当成 authoritative final state。

如果希望在服务端响应前先显示变化,应显式使用 optimistic update,再用真实响应校正。

为什么又不能什么都自动完成

实体字段更新容易合并,但列表关系更复杂。

例如创建新 Diagnosis:

Diagnosis:new 已进入 cache

不代表:

User:1.diagnoses 列表

自动知道应该插入它。

删除、排序、过滤列表也有同样问题,因为“实体存在”与“实体是否属于某个 query projection”不是同一事实。

所以仍可能需要:

cache.modify / update
writeFragment / writeQuery
refetchQueries
field policy

当前客户端写入与远端变化

当前客户端自己执行 Mutation

Client A
→ Mutation
→ Server
→ 返回 User:1 新状态
→ normalized merge
→ Client A UI 更新

这个场景通常不需要 Subscription 来告诉 Client A “你刚才自己改了”。

另一个客户端或后台任务修改

Client B / background job
→ Server state changed

Client A
→ 没有收到任何消息
→ 本地 User:1 仍可能是旧 projection

Normalized Cache 只负责收到数据以后怎么合并,并不负责凭空发现远端发生了变化。

因此还需要变化来源:

Subscription / SSE / WebSocket event
Polling
refetch on focus/reconnect

实时 payload 到达以后,才进入:

Event payload
→ identify User:1
→ merge / explicit cache update
→ UI re-render

所以:

Normalized Cache 解决“数据来了以后放到哪里、如何共享实体”;Subscription 等机制解决“远端变化怎样来到客户端”。

与 TanStack Query 的差异

TanStack Query 常以:

['user', 1]
['diagnosis', 'd1']

管理查询结果。

Normalized Cache 更强调:

Query result
   ↓ normalize
entities by identity
   ↓ references
many query projections

前者核心是 query identity,后者还增加 entity identity。

但不能反推出:

“TanStack Query 只能 invalidate/refetch,不能本地修改。”

TanStack Query 同样可以 setQueryData、optimistic update 或根据 mutation response patch query cache。差别主要在于缓存组织模型,不是“有没有修改缓存的 API”。

最重要的边界

客户端 cache 永远只是 server state 的 projection:

Server / DB = authority
Cache       = projection

Normalization 解决重复实体的一致性问题,但不能证明数据仍然最新,也不能替代服务端事务和权限控制。

常见误解

  • normalized cache = 数据库 normalization:不是。一个是客户端实体缓存,一个是关系数据库建模原则。
  • Apollo 根据 Mutation 参数自动相信新状态:默认不是;核心通常是对服务端返回实体做 normalization/merge。
  • Mutation 返回实体后所有列表都会正确:不一定,关系集合仍需 reconciliation。
  • 有 cache 就不需要 refetch:不一定。无法可靠推导的新状态仍适合重新向服务端取真值。
  • Normalized Cache 会自动知道别的用户更新了数据:不会,需要 Subscription、轮询或其他变化通知/重取机制。
创建于 2026/9/11 更新于 2026/9/15
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本