Published on

Relay 归一化机制导致的隐蔽问题

Authors
  • avatar
    Name
    hpoenixf
    Twitter

在一个使用 GraphQL + Relay 的项目中,我们遇到了一个非常隐蔽的问题: 页面没有报错,接口返回也看起来正常,但 UI 上的数据却偶尔不对。 后来发现,问题根本不在接口,而是在 Relay 的 store 机制。

业务背景

先说下业务场景。 系统里有「部门」的概念,一个用户可以同时属于多个部门。 在不同部门中,我们用同一个字段表示用户权限,比如 role。 但这个字段的值是和部门强相关的。

User A
- 在部门 X:role = admin
- 在部门 Y:role = viewer

从业务角度看,这非常合理。

初始的 Schema 设计

一开始,我们的 GraphQL Schema 是这样设计的:

type Department {
  id: ID!
  name: String!
  users: [User!]!
}

type User {
  id: ID!
  name: String!
  role: String!
}

查询部门列表时,直接把用户一起带出来:

query {
  departments {
    id
    name
    users {
      id
      name
      role
    }
  }
}

这个设计在非 Relay 项目里其实完全没问题, 但在 Relay 下,隐患已经埋好了。

问题现象

问题并不是一开始就出现的。 随着需求增加:

  • 页面开始同时展示多个部门
  • 同一个用户在不同部门下都会出现

这时开始出现一些很诡异的情况:

  • A 部门里明明是 admin
  • 切到 B 部门后再回来
  • A 部门里的角色变成了 viewer
  • 刷新页面,有时又恢复正常。

排查过程

最开始大家都在怀疑:

  • 后端是不是缓存有问题
  • 接口是不是返回顺序不稳定
  • 前端是不是 setState 写错了

后来直接打 log 看 Relay store,才发现真相。

问题根因:Relay 的归一化机制

Relay 会基于 id 对数据做归一化存储。 简单说就是:

同一个 id,在整个应用里只能有一份数据

于是发生了下面的事情:

  1. 请求部门 X

    • 返回 User A (role = admin)
  2. 请求部门 Y

    • 返回 User A (role = viewer)
  3. Relay 认为这是同一个 User

    • 后一次直接覆盖前一次

从 Relay 的角度看,它没做错。 错的是我们告诉它:这两个对象是同一个 Entity。

一个看似合理但无效的尝试

当时前端的第一反应是:

那我不请求 id 行不行?

于是尝试把查询改成:

users {
  name
  role
}

但很快发现问题依旧。 原因是: 只要 Schema 里这个类型有 id,Relay 还是会自动把 id 加进请求里。 也就是说,你根本绕不开它。

回头看:其实是建模问题

后来我们意识到一个关键点: "用户在某个部门下的身份 + 权限" 并不是一个纯粹的 User。 它是一个强上下文相关的数据结构。 但我们却把它建模成了一个全局 Entity。

最终方案:引入上下文对象

最终的解决方案其实很简单,但需要转一下思路。

不再直接返回 User

我们新增了一个类型:

type DepartmentUser {
  name: String!
  role: String!
}

并调整 Department:

type Department {
  id: ID!
  name: String!
  users: [DepartmentUser!]!
}

关键点只有一个:

DepartmentUser 没有 id

为什么这个方案能解决问题

因为在 Relay 里:

  • 没有 id → 不会被归一化
  • 不会进全局 store
  • 每次查询的数据都是独立的

即使是同一个用户:

  • 在不同部门下
  • 返回的数据也不会互相覆盖

一点额外收益

这个改动还有一个副作用(是好的那种):

  • Schema 语义更清晰了
  • 后端也更容易理解
  • 不再纠结「User 的 role 到底是谁的」

最后的经验总结

这次问题之后,我们在团队里约定了一条规则:

只给真正的全局实体加 id

如果一个对象:

  • 强依赖上下文
  • 离开父节点就没有意义

那它大概率不应该是 Relay Entity。

这个坑本身不复杂, 但如果你刚好踩到,真的会浪费不少时间。 希望这篇文章能帮到你。