← [ BACK_TO_NOTES ]

GraphQL Mutation 与错误语义

理解 GraphQL Mutation 作为显式写操作入口,并区分 Schema 校验、Runtime Validation、业务规则、数据库约束与错误翻译的职责。

#synthesis #growing #api #graphql

[!info] related notes

GraphQL Mutation 与错误语义

为什么要一起理解

Mutation 不只是“GraphQL 版 POST”。真正需要掌握的是:写操作如何表达副作用,输入在哪些层被验证,失败怎样翻译,以及客户端怎样让缓存重新与服务端一致。

Mutation 是什么

GraphQL 约定把可能改变服务端状态的操作放在 Mutation 根类型:

type Mutation {
  updateProfile(input: UpdateProfileInput!): UpdateProfilePayload!
}

客户端:

mutation UpdateProfile($input: UpdateProfileInput!) {
  updateProfile(input: $input) {
    profile { id name }
  }
}

Query 与 Mutation 最大的语义差异不是 HTTP method,而是:Mutation 明确告诉系统这里存在业务副作用。

Mutation 也不等于数据库 INSERT/UPDATE/DELETE。它可以表示:

createAssessment
sendEmail
cancelRun
generateReport
logout

更接近 Command / Use Case 的 API 入口。

为什么写操作通常使用 Input

input UpdateProfileInput {
  name: String
  height: Int
}

Input Object 可以让参数结构稳定演进,避免根字段长期堆积大量位置参数。

输入验证不是一层完成

假设:

input CreateUserInput {
  email: String!
  age: Int!
}

业务要求:

email 必须是合法邮箱
age >= 18
email 不能重复

不同层的职责不同。

1. GraphQL Schema:结构与基础类型

默认 String! / Int! 可以保证:

字段是否存在
是否允许 null
值是否属于 GraphQL 基础类型
输入结构是否符合 Schema

但默认不会自动保证:

email 真的是合法邮箱
age >= 18
score 在 0..100

除非额外使用自定义 Scalar、Directive 等机制。

2. Runtime Validation:格式与边界

例如 Zod:

const inputSchema = z.object({
  email: z.string().email(),
  age: z.number().int().min(18),
})

这里适合处理格式、范围以及 transport/application boundary 能明确判断的输入条件。

3. Service / Domain:业务不变量

例如:

当前状态是否允许修改?
用户今天是否还能创建 Assessment?
当前 actor 是否拥有该资源?

这些需要业务上下文,应该由 Service / Domain / Policy 负责。

4. Database Constraint:最终持久化边界

例如:

UNIQUE(email)
CHECK (...)
FOREIGN KEY (...)
NOT NULL

Service 可以提前检查 email 是否存在以提供友好错误,但不能替代 UNIQUE(email)。

并发场景:

Request A → 检查不存在
Request B → 检查不存在
A → INSERT
B → INSERT

只有数据库 UNIQUE 才能在最终持久化边界阻止重复。

所以不是“重复做同一件事”,而是:

GraphQL Schema
→ shape / primitive type

Runtime Validation
→ format / range

Service / Domain
→ business invariant / authorization

Database Constraint
→ concurrency-safe persistence invariant

GraphQL 错误要分层

1. Transport 错误

例如连接失败、网关超时、HTTP 无法到达 GraphQL Server。

此时通常连 GraphQL execution 都没真正完成。

2. Request / Validation 错误

查询不符合 Schema:

query {
  user {
    fieldThatDoesNotExist
  }
}

这类错误发生在执行 resolver 之前。

3. Execution 错误

Resolver 真正执行后发生异常,例如下游服务失败或非空字段无法满足契约。

GraphQL 允许返回:

{
  "data": {
    "user": {
      "name": "Sean",
      "diagnosis": null
    }
  },
  "errors": [
    { "message": "diagnosis service unavailable" }
  ]
}

也就是说:data 与 errors 可以同时存在。

这就是 GraphQL 很重要的“部分成功”语义。

4. 业务错误

例如:

用户名已存在
当前状态不允许修改
没有该资源的写权限

它们不一定应该全部变成未知的 resolver exception。稳定业务失败可以进入明确的 payload/union/code,让客户端可靠分支处理。

不要把数据库原始错误直接泄漏出去

例如 Prisma/PostgreSQL 报:

P2002
Unique constraint failed ...

不应默认直接透传给 GraphQL Client。

更合理的链路:

PostgreSQL / Prisma error
        ↓
Persistence Adapter / Repository translate
        ↓
EmailAlreadyExistsError
        ↓
GraphQL error mapping
        ↓
public error code + safe message

例如:

{
  "errors": [
    {
      "message": "Email already exists",
      "extensions": {
        "code": "EMAIL_ALREADY_EXISTS"
      }
    }
  ]
}

这样 API Contract 不被具体 ORM / DB 错误格式绑死,也减少内部实现信息泄漏。

Nullability 会影响错误传播

如果某字段声明为非空:

name: String!

但 resolver 返回 null,GraphQL 必须把这个契约违例向上冒泡,直到找到允许 null 的父级。

所以 ! 不只是“类型更严格”,还直接影响运行时错误传播范围。

Mutation 后还没结束

写成功之后,客户端缓存可能仍然是旧数据。

因此完整链路是:

Mutation
  ↓
Server state changed
  ↓
Mutation response
  ↓
Client cache reconcile
  ↓
UI becomes consistent

在 Apollo Client 中,Mutation 返回具有稳定 identity 的实体时,normalized cache 可以 merge 对应 entity;列表 membership、派生查询或响应信息不足时,仍可能需要手动 cache update 或 refetch。

在 TanStack Query 中,也可以直接 setQueryData / optimistic update;invalidateQueries + refetch 是常见的重新获取 authority 的策略,而不是唯一能力。

判断准则

看到 GraphQL 写操作时依次问:

Schema 能验证什么?
→ Runtime validation 还要验证什么?
→ Service / Domain 要守什么业务规则?
→ Database Constraint 要守什么最终不变量?
→ 底层错误怎样翻译成稳定 API 错误?
→ Mutation 后客户端 projection 怎样重新一致?

只有先分层,验证、重试、用户提示、监控、授权和缓存恢复才不会混在一起。

创建于 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
关闭浮层 / 取消命令;输入框内清空文本