GraphQL 一次请求的端到端执行
用一个完整的前后端案例解释 GraphQL Schema 如何成为接口合同,以及客户端任意字段组合如何由执行引擎和 Resolver 在服务端落地。
[!info] related notes
- 所属 MOC: graphql-moc
- 相关概念:
- 易混淆概念:
- 相关资源:
GraphQL 一次请求的端到端执行
要解决的问题
GraphQL 最容易产生的疑问是:
前端既然可以自由组合字段,难道后端要为每一种组合提前写一个接口吗?
答案是 不需要。
GraphQL 的关键设计是:后端不是预先实现“组合”,而是实现一组 可组合的字段能力;GraphQL 执行引擎根据客户端这一次的 selection set,把需要的字段拼成一棵执行树。
下面用一个简化的 BodySense 风格案例走完整链路。
1. 后端先声明 Schema
页面可能需要用户、最新诊断和当前训练计划:
type User {
id: ID!
name: String!
latestDiagnosis: Diagnosis
currentTrainingPlan: TrainingPlan
}
type Diagnosis {
id: ID!
summary: String!
}
type TrainingPlan {
id: ID!
title: String!
}
type Query {
user(id: ID!): User
}
这份 Schema 表示:
客户端允许从 Query.user 进入
↓
拿到 User 后可以继续选择
name / latestDiagnosis / currentTrainingPlan
↓
嵌套对象又有自己的可选字段
它只声明 允许查询什么,不说明这些数据具体存在哪里。
2. 后端实现 Resolver
const resolvers = {
Query: {
user: (_parent, { id }, ctx) =>
ctx.userService.getById(id),
},
User: {
latestDiagnosis: (user, _args, ctx) =>
ctx.diagnosisService.getLatestByUserId(user.id),
currentTrainingPlan: (user, _args, ctx) =>
ctx.trainingService.getCurrentByUserId(user.id),
},
};
这里后端只实现几个稳定能力:
Query.user
User.latestDiagnosis
User.currentTrainingPlan
并没有实现:
user + diagnosis
user + trainingPlan
user + diagnosis + trainingPlan
……
这些组合由客户端查询在运行时组成。
3. 前端声明这次真正需要什么
页面 A 只需要名字:
query GetUserName($id: ID!) {
user(id: $id) {
name
}
}
页面 B 需要名字和诊断:
query GetUserDiagnosis($id: ID!) {
user(id: $id) {
name
latestDiagnosis {
summary
}
}
}
页面 C 再加训练计划:
query GetWorkspace($id: ID!) {
user(id: $id) {
name
latestDiagnosis {
summary
}
currentTrainingPlan {
title
}
}
}
后端不需要因此增加三个 endpoint。
4. 请求怎样发送
GraphQL 常通过一个 HTTP endpoint,例如:
POST /graphql
请求体大致是:
{
"query": "query GetWorkspace($id: ID!) { ... }",
"variables": {
"id": "u1"
}
}
真正决定“要哪些字段”的不是 URL,而是请求中的 GraphQL operation。
5. 服务端执行引擎做什么
收到 Query 后,GraphQL Server 大致经历:
Parse
↓
Validate against Schema
↓
Execute selection set
↓
call Query.user resolver
↓
得到 User
↓
只继续解析本次被选中的字段
├─ name
├─ latestDiagnosis → resolver
└─ currentTrainingPlan → resolver
↓
assemble response
可以把页面 C 看成一棵执行树:
Query.user(id: "u1")
├─ name
├─ latestDiagnosis
│ └─ summary
└─ currentTrainingPlan
└─ title
GraphQL 执行引擎按照这棵树组织调用和结果。
6. 返回结果
{
"data": {
"user": {
"name": "Sean",
"latestDiagnosis": {
"summary": "..."
},
"currentTrainingPlan": {
"title": "Lower Body A"
}
}
}
}
查询形状与响应形状基本对应,因此前端能明确知道自己拿到什么。
为什么“任意组合”能够成立
核心不是后端支持无限种特殊接口,而是:
REST 常见思路
后端定义一组完整 response endpoint
GraphQL 思路
后端定义一组可组合 field contract
↓
客户端在允许范围内组合 selection set
↓
执行引擎按字段解析
因此“自由”是 Schema 内的组合自由,不是客户端可以随便查询数据库。
Schema 怎样替代传统接口文档的一部分
在 REST 团队里常见:
OpenAPI / Swagger / Apifox 文档
→ 前端查 URL、method、request、response
GraphQL 中,Schema 本身就是机器可读合同:
type Query {
user(id: ID!): User
}
GraphQL 还定义了 introspection,工具可以向服务端查询 Schema 信息。
因此 GraphQL IDE、文档浏览器和 codegen 可以从同一个 Schema 获得:
有哪些 Query / Mutation
有哪些类型和字段
参数是什么类型
哪些字段 nullable
字段之间有哪些关系
常见团队链路是:
Backend Schema
↓ publish / introspection / registry
GraphQL tooling
├─ API Docs
├─ autocomplete
├─ validation
└─ TypeScript codegen
↓
Frontend operations
这比“人手维护一份与代码可能漂移的接口表格”更接近 executable contract。
一个非常重要的性能边界
前端只查询:
user { name }
并 不保证数据库只 SELECT name。
如果 userService.getById() 内部仍然执行:
SELECT * FROM users WHERE id = ?;
数据库依旧可能读取更多列,只是 GraphQL 最终不会把未选择字段返回给客户端。
所以:
GraphQL 控制的是 API response selection;数据库查询优化仍然属于 Resolver / Service / Repository 的责任。
同理,嵌套字段还可能造成 N+1,需要 batch、JOIN、DataLoader 等手段治理。
最终心智模型
Schema
= 后端公开能力边界
Operation
= 前端这一次从能力边界中选择什么
Execution Engine
= 根据 selection set 调度字段解析
Resolver
= 一个字段怎样连接到真实业务数据
Service / Repository
= 真正业务规则和数据访问
前端的自由,本质上来自 对稳定字段能力的组合,而不是后端为所有组合提前写代码。