REST与GraphQL:两种架构背后的真实权衡
解释了 REST 和 GraphQL 分别解决的具体问题、其内部机制,以及在为 API 选择其中一种技术时需要权衡的隐性代价。
来源: REST vs GraphQL: Which Problem Does Each One Actually Solve?
当着手设计客户端与服务器之间的通信方式时,几乎每次都会出现一个问题:选择 REST 还是 GraphQL?
关于这个话题的争论往往将其视为一场竞赛,好像必须有一方胜出。有些开发者认为 REST 已经过时,应该被淘汰;而另一些则坚持认为 GraphQL 带来了大多数项目并不需要的复杂性。这两种观点都并无太大意义。
技术并非为在争论中得分而设计的,它们的存在是为了解决开发者在特定情况下会遇到的实际问题。
仅仅因为 GraphQL 看起来更时髦就采用它,可能会让你陷入缓存问题以及那些原本未预料到的昂贵服务器端查询的困扰之中。相反,若在数据量大的移动应用上强行使用严格的 REST 结构,则会让界面因大量连续的网络请求而变得卡顿。
要做出明智的决定,最好先抛开炒作,深入探究其背后的动机:每种方法旨在解决什么问题、其内部运作机制如何,以及会带来哪些权衡。
REST解决了什么问题?
在REST成为标准之前,Web服务严重依赖SOAP和XML-RPC等协议。这些协议要求使用严格的XML架构、冗长的消息格式以及复杂的设置流程。客户端仅为了获取数据就需要知道精确的远程过程名称。
Roy Fielding于2000年提出了REST(表述性状态转移)概念。
其核心思想是一个简单而有效的理念:利用Web自身的架构。
REST并非将服务器视为不可理解的功能执行引擎,而是把系统建模为一系列资源的集合,每个资源都可以通过简洁的URL来访问。对这些资源的操作通过标准的HTTP动词(GET、POST、PUT、DELETE)来表达,而每个请求的结果则通过常见的HTTP状态码来反馈,例如表示成功的200 OK、资源不存在时的404 Not Found,或是后端出现故障时的500 Server Error。
Resource-Oriented Architecture (REST)
Client ─── GET /api/users/12 ──────────> Server (Find user #12)
Client <── 200 OK [ { user data } ] ──── Server
Client ─── DELETE /api/posts/99 ───────> Server (Delete post #99)
Client <── 204 No Content ───────────── Server
为什么REST很重要
- 简洁性与可预测性:一旦明白
/api/products指的是产品集合,而/api/products/42指向某个特定产品,该接口就会变得一目了然。
GET 请求是可重试的且对应唯一的 URL,浏览器、CDN(如 Cloudflare)以及反向代理服务器(如 Nginx)等基础设施无需额外应用逻辑即可直接缓存响应。这种模式长期以来为开发者提供了良好的支持。但随着客户端应用日益复杂,一些问题开始显现。
新的痛点:过度获取与获取不足
想象一下某个移动应用正在渲染用户个人资料页面。该页面恰好需要三样内容:
- 用户的姓名
- 指向用户头像的 URL
- 用户最近三篇博客文章的标题
使用传统的 REST API 时,满足这一简单需求往往会导致两个问题。
1. 过度获取
客户端请求 GET /api/users/12。
服务器返回完整的用户记录:
{
"id": 12,
"name": "Sarah Connor",
"avatarUrl": "https://cdn.example.com/sarah.jpg",
"email": "sarah@example.com",
"address": { "street": "...", "zip": "...", "country": "..." },
"billingHistory": [ ... ],
"preferences": { ... },
"createdAt": "2021-04-12T08:14:00Z"
}
屏幕实际上只需要 name 和 avatarUrl,但却获取了无关的地址详情、账单信息以及偏好设置。在速度较慢的蜂窝网络环境下,传输不必要的数据会浪费电量并降低页面加载速度。这种情况被称为过度获取。
2. 获取不足(N+1 网络问题)
客户端仍然需要 Sarah 的文章,但 /api/users/12 的响应中并未包含这些内容。
因此它会发起第二次请求:GET /api/users/12/posts。
假设该接口仅返回帖子标识符的列表。在这种情况下,客户端可能需要进行三次独立的后续请求,分别获取/api/posts/101、/api/posts/102和/api/posts/103的内容。
仅仅为了显示一个页面,设备就已经进行了多次连续的网络往返操作。这种模式被称为数据获取不足。
GraphQL究竟解决了什么问题?
2012年左右,Facebook在将其核心应用从移动网页版本转向原生应用时遇到了同样的瓶颈。当时的移动网络既不可靠又速度缓慢,而Facebook的界面是由彼此紧密关联的数据构成的——用户、好友、评论、点赞、反应等。
为每个界面设计专用的 REST 接口,比如 /api/mobile-profile-screen,很快就会变成一种难以管理的模式。
为了解决这个问题,Facebook 开发了 GraphQL。
GraphQL 不是一种数据库技术,而是一种API 查询语言,它配有服务器端运行时环境,能够根据你定义的类型系统来执行查询。
客户端无需多次往返于不同的接口,只需发送一个包含结构化查询的 POST 请求,明确说明所需数据的格式。服务器则会返回完全符合该格式的 JSON 数据。
GraphQL 的实现方式
以下是使用 GraphQL 请求同一个人资料界面的数据的示例:
GraphQL
query GetUserProfile {
user(id: "12") {
name
avatarUrl
posts(limit: 3) {
title
}
}
}
服务器只会返回恰好符合请求内容的数据,不会多给任何多余信息:
JSON
{
"data": {
"user": {
"name": "Sarah Connor",
"avatarUrl": "https://cdn.example.com/sarah.jpg",
"posts": [
{ "title": "Building Reliable Systems" },
{ "title": "Understanding Memory Leaks" },
{ "title": "A Guide to Clean Architecture" }
]
}
}
}
不存在因未使用字段而造成的带宽浪费,也无需多次请求来获取相关数据。仅需一次网络调用即可获得视图渲染所需的数据。
深入探究
要真正理解其中的权衡,了解每种方法在服务器端的执行方式会有所帮助。
REST的基于端点的模型
在Express.js这样的框架中,路由会将传入的URL直接映射到处理函数:
JavaScript
// Express.js REST route
app.get('/api/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return res.status(404).json({ error: 'User not found' });
res.json(user);
});
后端开发人员可以完全掌控查询逻辑、数据库操作以及响应的结构。在这种架构下进行性能优化非常简单:只需直接修改相应的端点处理函数,优化SQL语句或添加缓存即可。
GraphQL的架构与解析器模型
GraphQL 是以Schema为核心,结合被称为Resolvers的各个函数构建的:
JavaScript
// Type definitions
const typeDefs = `
type Post {
id: ID!
title: String!
}
type User {
id: ID!
name: String!
avatarUrl: String!
posts: [Post]
}
type Query {
user(id: ID!): User
}
`;
// Resolvers map fields to actual data sources
const resolvers = {
Query: {
user: async (_, { id }) => {
return await db.users.findById(id);
},
},
User: {
posts: async (parentUser) => {
// Runs when the client query includes the 'posts' field
return await db.posts.findByAuthorId(parentUser.id);
},
},
};
当有查询请求到来时,GraphQL 引擎会先根据 Schema 对其进行校验,然后按树状结构依次调用各个 Resolver 并执行它们。
这为前端团队在构建请求时提供了极大的灵活性。但有一个值得注意的弊端:虽然客户端决定查询的格式,但实际上承担处理成本的是服务器。
GraphQL 在幕后带来的代价
这种灵活性并非免费获得。从 REST 转向 GraphQL 实际上是将复杂性从客户端转移到了服务器端。
数据库层中的 N+1 查询问题
再来看看上面的解析器配置。如果客户端请求列出20名用户以及每位用户的帖子,会发生什么?
GraphQL
query {
users {
name
posts {
title
}
}
}
users解析器只会执行一次,一次性获取全部20条记录(SELECT * FROM users)。- 随后,
posts解析器会针对返回的**每位用户**分别发起查询。 - 最终结果:数据库需要处理1次用于获取用户列表的查询,再加上20次用于获取帖子的查询——总共21次查询。
在REST架构中,通常可以在端点代码中使用SQL连接将这些查询合并为一次查询(SELECT * FROM users JOIN posts...)。而使用GraphQL时,则需要像DataLoader这样的批处理工具,在查询到达数据库之前将它们汇总起来。
HTTP缓存带来的问题
试想这样一个 REST 调用:
GET /api/articles/42
CDN 可以愉快地将该响应缓存大约 10 分钟。如果在这段时间内有 5,000 名用户访问 /api/articles/42,您的源服务器只需处理第一个请求——其余的都由 CDN 承担。
GraphQL 打破了这种模式,因为几乎所有操作都是以 POST 请求形式发送到同一个端点 /graphql。
由于实际查询内容位于请求体中而非 URL 中,普通的 HTTP 缓存、代理和 CDN 都无法识别出有两个请求正在获取相同的数据。为了解决这个问题,您需要在客户端使用复杂的标准化缓存(例如 Apollo Client 或 Relay),或者使用专门设计的缓存网关。
查询深度作为攻击向量
在 REST 中,攻击者没有简单的方法能够通过单个端点遍历深度任意大的关系型数据。
然而在 GraphQL 中,恶意行为者可以提交类似这样的内容:
GraphQL
query EvilQuery {
user {
posts {
author {
posts {
author {
posts {
title
}
}
}
}
}
}
}
如果没有查询复杂度评分或深度限制等保护措施,这样的查询就会导致数据库运行缓慢甚至耗尽服务器的可用内存。
REST 与 GraphQL 对比
特性:REST、GraphQL
数据获取:每个URL返回结构固定的数据,仅返回客户端请求的字段
网络调用:通常需要多次往返,一般可在一次请求中完成解析
HTTP缓存:内置功能,简单高效,但实现较为困难,需要在客户端或网关上使用额外工具
学习曲线:较平缓,基于常见的HTTP规范;较为陡峭,需要理解数据架构和解析器
错误报告:使用常规的HTTP状态码(404、500、401);几乎总是返回200 OK,问题会列在errors字段中
文件上传:通过多部分表单数据即可实现,但需要自定义变异逻辑或预签名上传URL
理想应用场景:CRUD风格的服务、微服务、开放Web API,以及跨多个平台的丰富UI和应用
如何选择:决策方法
何时REST更合适
- 您的资源模型较为简单。对于管理面板、控制台或基础商店页面这类典型的CRUD驱动型产品,REST能帮助您更快地完成开发与部署。
- 缓存对您而言非常重要。对于博客、新闻页面或产品目录这类面向公众且以读取为主的内容,通过CDN实现的原生HTTP缓存在速度和成本方面都难以被超越。
- 您正在为外部用户提供API。外部开发者无需学习专门的客户端库,即可使用
curl或fetch来访问REST接口。
何时选择GraphQL更合适
- 您需要支持多种不同的客户端类型。比如某个产品同时拥有iOS、Android、智能手表以及网页端客户端,而这些客户端各自需要同一底层数据的不同子集。
总结
这两种技术并未因彼此的存在而变得过时。
- REST解决了通过网络以一致、可扩展且易于缓存的方式暴露资源的需求。
- GraphQL则满足了客户端能够自行构建数据请求的需求,从而减少了在速度慢或不可靠的移动网络连接下产生的不必要的数据传输。
在两者之间做出选择并非是为了追随潮流,而是为了了解系统面临的实际瓶颈。如果你的最大挑战是移动端性能以及日益增多的前端专用接口,GraphQL可以成为一个很好的解决方案。而如果你最看重的是简洁性、可靠的缓存机制以及易于维护的后端,REST依然是一个经过验证的可靠选择。