GraphQL Query 与 Resolver 的执行链
把 GraphQL Query 的字段选择与 Resolver 的逐字段执行连接起来,理解 Schema、Resolver、Context、中间件、Service 与数据库之间的工程边界。
[!info] related notes
- 所属 MOC: graphql-moc
- 相关概念: graphql-auth-context、graphql-n-plus-one
- 易混淆概念: graphql-vs-rest
- 相关资源: graphql-end-to-end-request-flow
GraphQL Query 与 Resolver 的执行链
为什么要一起理解
Query 是客户端声明“我要什么”,Resolver 是服务端回答“这个字段怎么得到”。二者通过 Schema 中的字段定义连接。
type Query {
user(id: ID!): User
}
type User {
id: ID!
name: String!
latestDiagnosis: Diagnosis
}
客户端:
query GetUser($id: ID!) {
user(id: $id) {
name
latestDiagnosis { summary }
}
}
执行链
可以把一次 GraphQL 查询看成一棵字段执行树:
Query.user(id)
↓ resolver
User
├─ name
└─ latestDiagnosis
↓ resolver
Diagnosis.summary
典型 resolver 接收四类信息:
parent 上一级字段返回的对象
args 当前字段参数
context 本次请求共享上下文
info 当前 GraphQL 执行信息
例如:
const resolvers = {
Query: {
user: (_parent, { id }, ctx) => ctx.userService.getById(id),
},
User: {
latestDiagnosis: (user, _args, ctx) =>
ctx.diagnosisService.getLatestByUserId(user.id),
},
};
Resolver 与 REST Controller 的关系
REST 常见执行入口是:
Route → Controller / Handler → Service → Repository
GraphQL 更像:
Schema field → Resolver → Service → Repository
其中 Root Resolver:
Query.user
Mutation.updateUser
非常接近 REST Controller / Handler:负责接 API 参数、读取请求上下文、调用 Use Case / Service、把结果映射回 API contract。
但 GraphQL 还有 REST Controller 没有完全一一对应的 Nested Field Resolver:
User.assessments
Assessment.owner
Goal.tasks
它们是在字段级继续解析关系数据。因此更准确的说法是:
Resolver 是 GraphQL Schema 与应用层之间的 Adapter;Root Resolver 很像 Controller,Nested Resolver 更像字段级的数据解析 Adapter。
Resolver 不应该成为业务层
教学示例经常直接写:
User: {
assessments: user =>
prisma.assessment.findMany({ where: { userId: user.id } })
}
这能解释 Resolver 如何取数据,但真实工程更推荐:
Resolver
↓
Service / Use Case
↓
Repository
↓
Prisma / SQL
↓
Database
例如:
User: {
assessments: (user, _args, ctx) =>
ctx.assessmentService.listByUserId(user.id)
}
业务规则、事务编排、资源级授权应尽量留在 Service / Policy,而不是散落在 GraphQL transport 层。
Schema 不会自动查询数据库
type Query {
user(id: ID!): User
}
只表示:
API 有一个 user 字段
需要 ID 参数
返回 User
它并没有规定:
SELECT * FROM users WHERE id = ?
真正的数据来源由 Resolver / Service 决定,可以是:
PostgreSQL
Redis
REST API
另一个 GraphQL service
文件
纯计算
所以 GraphQL 不会自动决定 JOIN,也不会自动优化 SQL。
Selection Set 决定本次需要哪些字段
Schema 中存在:
type User {
name: String!
assessments: [Assessment!]!
}
不代表每次获取 User 都会加载 assessments。
如果客户端只请求:
user(id: "42") {
name
}
则 User.assessments 的自定义 Resolver 通常不会执行。
只有请求:
user(id: "42") {
name
assessments { score }
}
才需要沿着 User.assessments 继续解析。
中间件与请求边界在哪里
GraphQL 并没有把所有 middleware 消灭,而是出现了多个边界。
1. HTTP Middleware
GraphQL 请求通常仍通过 HTTP 进入:
POST /graphql
↓
CORS
request-id
logging
rate limiting
body size / compression
↓
GraphQL HTTP handler
这些仍然适合普通 Web 框架 middleware。
2. GraphQL Context
每一次 GraphQL Request 建立一次 request-scoped context:
authenticate credential
request / trace id
services
request-scoped DataLoaders
然后这一请求中的所有 Resolver 共享它。
3. Resolver wrapper / directive / plugin
一些 GraphQL 特有横切逻辑可以放在:
resolver wrapper
schema directive
server plugin / hook
但不要把复杂业务授权全部抽成“万能 middleware”。
4. Service / Policy
真正依赖资源状态的 Authorization:
User 42 能否修改 Assessment 789?
通常应由 Service / Policy 判断,因为它需要结合数据库中的 ownership、组织关系、资源状态等事实。
Context 的正确角色
context 适合放“本次请求共享且可信的依赖/身份信息”,例如:
currentUser / principal
requestId
services
request-scoped loaders
它不应该成为随意塞全局状态的垃圾桶。
认证通常先在 transport/request 边界解析 credential,再把得到的可信 identity 放入 context;resolver 把 actor identity 传给 Service,Service / Policy 完成资源级授权。
Context
→ 你是谁?
Service / Policy
→ 你能不能访问这个具体资源?
最重要的不变量
- Query 只能选择 Schema 公开的字段;
- Resolver 决定字段如何计算,而不是 Schema 自动查数据库;
- Resolver 可以访问数据库、Service、其他 API,也可以纯计算;
- Selection Set 决定本次实际需要解析哪些字段;
- 同一个对象字段可能被大量父对象反复调用,因此逐字段执行很容易引出 N+1;
- Resolver 应保持薄:业务规则仍应放在可复用 Service/Domain 层,而不是散落在 GraphQL 层。
常见误解
- 每个字段都必须写 resolver:不一定。许多实现对同名属性支持默认 resolver。
- GraphQL 自动优化 SQL:不会。GraphQL 只知道字段执行,不知道你的数据库最佳查询计划。
- Resolver 就是业务层:不推荐。它更接近 API adapter/handler。
- Resolver 就完全等于 REST Controller:Root Resolver 很接近,但 Nested Field Resolver 是 GraphQL 特有的重要差异。
- Schema 定义了关系字段就一定会查关系数据:不会;客户端没选择该字段时,对应 Resolver 通常不会执行。