GraphQL vs REST——我做过两个项目的对比,我的结论
GraphQL vs REST——我做过两个项目的对比,我的结论
适读人群:正在选型或考虑从 REST 迁移到 GraphQL 的工程师 | 阅读时长:约15分钟 | 核心价值:GraphQL 解决了 REST 的某些真实痛点,但也带来了新的问题——不是所有项目都需要 GraphQL
我做 GraphQL 的背景
2021 年,我们在做一个新的 BFF(Backend For Frontend)层重构。原来的做法是每个前端页面对应一个后端接口,十几个页面就有十几个接口,每个接口的字段都不完全一样。
前端同学抱怨说,一个新页面上线,总是要等后端同学专门开发一个新接口,即使数据大部分是现有的,只是字段组合方式不同。
我们决定试试 GraphQL,用它来代替这些 BFF 接口。
这一试就是两年。在这两年里,我亲身经历了 GraphQL 的优雅之处,也踩了很多 REST 时代没有的坑。
GraphQL 解决了什么问题
问题一:Over-fetching(过度获取)
REST 接口通常返回固定的字段集合。比如 /users/{id} 返回用户的所有信息:
{
"id": 1,
"name": "张三",
"email": "zhangsan@example.com",
"phone": "138****8888",
"avatar": "https://xxx.com/avatar.jpg",
"address": {...},
"preferences": {...},
"lastLoginTime": "...",
// 30个字段
}但这个页面只需要显示用户名和头像,其他 28 个字段白传了,浪费带宽。
GraphQL 让客户端精确指定需要哪些字段:
query {
user(id: 1) {
name
avatar
}
}响应只包含请求的字段:
{
"data": {
"user": {
"name": "张三",
"avatar": "https://xxx.com/avatar.jpg"
}
}
}问题二:Under-fetching(获取不足)
一个页面需要展示用户信息 + 用户的订单列表 + 每个订单的商品列表,在 REST 里需要多次请求:
GET /users/1 // 获取用户
GET /orders?userId=1 // 获取用户订单
GET /orders/101/items // 获取订单1的商品
GET /orders/102/items // 获取订单2的商品4 次请求,而且后面的请求依赖前面的结果,必须串行。
GraphQL 一次请求搞定:
query {
user(id: 1) {
name
orders {
id
status
items {
productName
quantity
price
}
}
}
}这对移动端来说非常有价值,减少了网络往返次数,改善了弱网下的体验。
GraphQL 带来了什么新问题
问题一:N+1 查询问题
GraphQL 的嵌套查询特性,如果实现不当,会导致 N+1 查询问题。
# 查询 10 个订单,每个订单的商品信息
query {
orders { # 1 次查询
items { # 10 次查询(每个订单单独查一次)
product {
name
}
}
}
}如果 items 的 resolver 直接查数据库,就会产生 1 + N 次查询(先查订单,再对每个订单查商品)。
解决方案是 DataLoader(批量加载):
// DataLoader:把多个独立的查询合并成一次批量查询
const itemsLoader = new DataLoader(async (orderIds) => {
// 一次性查所有 orderIds 对应的 items
const items = await db.query(
'SELECT * FROM order_items WHERE order_id IN (?)', [orderIds]
);
// 按 orderId 分组返回
return orderIds.map(id => items.filter(item => item.orderId === id));
});DataLoader 是 GraphQL 必须掌握的工具,但这是 REST 时代不需要考虑的问题。
问题二:HTTP 缓存失效
REST 天然支持 HTTP 缓存:GET 请求可以被 CDN、浏览器缓存。
GraphQL 的查询通常走 POST(因为查询语句在请求体里),POST 请求是不会被 HTTP 缓存的。
解决方案有两种:
- 使用 GET 请求发送 GraphQL 查询(query 放在 query string 里),但复杂查询的 URL 会很长
- 使用 Persisted Queries(持久化查询):客户端发送查询的 Hash,服务端根据 Hash 找到对应的查询语句
问题三:错误处理变了
REST 用 HTTP 状态码表示错误(400、404、500 等)。
GraphQL 的约定是:HTTP 状态码总是 200,错误信息放在响应体的 errors 字段里:
{
"data": {
"user": null
},
"errors": [
{
"message": "用户不存在",
"path": ["user"],
"extensions": {"code": "USER_NOT_FOUND"}
}
]
}这对习惯 REST 错误处理的开发者来说需要适应,而且监控工具(比如看 HTTP 状态码来判断接口健康度)也需要改造。
问题四:Schema 设计成了核心工作
GraphQL 的强 Schema 要求:在开发接口之前,必须先设计好完整的 Schema(类型定义)。
这既是优点(Schema 是文档和契约),也是成本:Schema 设计需要时间和专业知识,Schema 的演进(添加字段、废弃字段)有专门的规范(不能直接删字段,要先标记 @deprecated),需要团队认真对待。
踩坑记录
踩坑一:Schema 设计初期就过度设计
我们第一版 Schema 把所有可能的查询条件、所有可能的关联关系都设计进去了,结果 Schema 定义文件有 3000 多行,新人看不懂,维护成本极高。
教训: Schema 从最简单的开始,只定义当前确实需要的。GraphQL 的演进成本低(加字段容易),不需要一开始就设计所有场景。
踩坑二:前端随意嵌套查询,性能失控
GraphQL 给了前端极大的自由度,但有些前端同学在不了解后端实现的情况下,写出了非常深的嵌套查询:
query {
user {
orders {
items {
product {
category {
parentCategory {
# 还有更多嵌套...
}
}
}
}
}
}
}这种查询在后端可能触发几百次数据库查询,直接把数据库打慢了。
解决:
- 设置查询深度限制(通常不超过 5 层)
- 设置查询复杂度限制(每个字段有成本权重,总成本超过阈值就拒绝)
// Apollo Server 设置查询复杂度限制
const server = new ApolloServer({
schema,
plugins: [
{
requestDidStart() {
return {
didResolveOperation({ document }) {
const complexity = getComplexity({
schema,
query: document,
variables,
estimators: [simpleEstimator({ defaultComplexity: 1 })]
});
if (complexity > 1000) {
throw new Error(`查询复杂度 ${complexity} 超过限制 1000`);
}
}
};
}
}
]
});踩坑三:监控和调试难度增加
REST 里每个接口有独立的 URL,监控系统可以按 URL 统计各接口的 QPS、响应时间、错误率。
GraphQL 只有一个 /graphql 端点,所有查询都从这里进,传统的 URL 维度监控失效了。
解决: 用 Apollo Studio 或者自定义 GraphQL 插件,把每个 operationName 作为监控维度:
// 所有 GraphQL 查询要有 operationName
query GetUserProfile { // "GetUserProfile" 就是 operationName
user(id: 1) {
name
avatar
}
}在后端记录日志和监控时,以 operationName 为维度统计。
我的最终结论
两年下来,我认为 GraphQL 在以下场景里有明显优势:
- 前端主导的产品:前端迭代快,经常需要不同字段组合,GraphQL 减少了后端配合成本
- 多客户端场景:Web、iOS、Android 需要的字段不完全一样,GraphQL 避免了为每个客户端维护独立接口
- 数据模型复杂、关联多:社交网络、电商详情页这类强关联的数据,GraphQL 的嵌套查询非常优雅
REST 在以下场景依然更好:
- 简单的 CRUD 接口:如果接口就是"增删改查用户",REST 更直接,GraphQL 引入的复杂度不值得
- 需要 HTTP 缓存:CDN 密集型的场景,REST 的 GET 缓存天然可用
- 团队对 GraphQL 不熟悉:引入新技术的学习成本不可忽视
- 强监控需求:现有监控体系已经很完善,GraphQL 的适配成本高
不要因为 GraphQL 流行就用 GraphQL,也不要因为"REST 更简单"就拒绝 GraphQL。根据你的团队状态和业务特点做选择。
