概述
在构建应用系统的Web服务时,我们通常会在REST和GraphQL两种通信模式中做选择。虽然两者都基于HTTP传输JSON数据,但各有优劣。
本文将通过实际的产品数据库案例,对比两种方案在相同客户端操作下的实现差异。我们不会啰嗦基础概念,直接上干货——毕竟读者都是经验丰富的开发者。
示例服务
我们的示例服务需要支持以下功能:
- 创建草稿状态的产品
- 更新产品详情
- 获取产品列表
- 获取单个产品及其关联订单
下面先用REST实现这些功能。
REST
REST(表述性状态转移)是分布式超媒体系统的架构风格,核心概念是"资源"。本例中的资源就是"产品"。
创建产品
使用POST方法创建产品:
curl --request POST 'http://localhost:8081/product' \
--header 'Content-Type: application/json' \
--data '{
"name": "Watch",
"description": "Special Swiss Watch",
"status": "Draft",
"currency": "USD",
"price": null,
"imageUrls": null,
"videoUrls": null,
"stock": null,
"averageRating": null
}'
执行后系统会创建新产品。
更新产品
REST惯例使用PUT方法更新:
curl --request PUT 'http://localhost:8081/product/{product-id}' \
--header 'Content-Type: application/json' \
--data '{
"name": "Watch",
"description": "Special Swiss Watch",
"status": "Draft",
"currency": "USD",
"price": 1200.0,
"imageUrls": [
"https://graphqlvsrest.com/imageurl/product-id"
],
"videoUrls": [
"https://graphqlvsrest.com/videourl/product-id"
],
"stock": 10,
"averageRating": 0.0
}'
这将更新指定ID的产品对象。
获取产品列表
列表查询通常是GET操作,通过查询参数控制分页:
curl --request GET 'http://localhost:8081/product?size=10&page=0'
返回的首个产品对象:
{
"id": 1,
"name": "T-Shirt",
"description": "Special beach T-Shirt",
"status": Published,
"currency": "USD",
"price": 30.0,
"imageUrls": ["https://graphqlvsrest.com/imageurl/1"],
"videoUrls": ["https://graphqlvsrest.com/videourl/1"],
"stock": 10,
"averageRating": 3.5
}
获取产品及关联订单
要获取产品和订单,通常需要两步:
- 先获取产品列表
- 再调用订单接口查询关联数据:
curl --request GET 'localhost:8081/order?product-id=1'
返回的订单对象:
{
"id": 1,
"productId": 1,
"customerId": "de68a771-2fcc-4e6b-a05d-e30a8dd0d756",
"status": "Delivered",
"address": "43-F 12th Street",
"creationDate": "Mon Jan 17 01:00:18 GST 2022"
}
这里踩了个坑:每个产品都需要额外发起订单查询,典型的N+1查询问题。
GraphQL
GraphQL是API的查询语言,自带查询执行框架。核心是查询(Query)和变更(Mutation),两者都通过Schema定义请求和响应结构。
我们用Spring GraphQL重新实现示例服务。
创建产品
使用saveProduct变更:
curl --request POST 'http://localhost:8081/graphql' \
--header 'Content-Type: application/json' \
--data \
'{
"query": "mutation {saveProduct (
product: {
name: \"Bed-Side Lamp\",
price: 24.0,
status: \"Draft\",
currency: \"USD\"
}){ id name currency price status}
}"
}'
圆括号内是输入类型Schema,花括号指定返回字段。响应:
{
"data": {
"saveProduct": {
"id": "12",
"name": "Bed-Side Lamp",
"currency": "USD",
"price": 24.0,
"status": "Draft"
}
}
}
更新产品
使用updateProduct变更:
curl --request POST 'http://localhost:8081/graphql' \
--header 'Content-Type: application/json' \
--data \
'{"query": "mutation {updateProduct(
id: 11
product: {
price: 14.0,
status: \"Publish\"
}){ id name currency price status }
}","variables":{}}'
响应包含指定字段:
{
"data": {
"updateProduct": {
"id": "12",
"name": "Bed-Side Lamp",
"currency": "USD",
"price": 14.0,
"status": "Published"
}
}
}
GraphQL提供了响应格式的灵活性,客户端可以按需选择返回字段。
获取产品列表
使用查询获取数据:
curl --request POST 'http://localhost:8081/graphql' \
--header 'Content-Type: application/json' \
--data \
'{
"query": "query {products(size:10,page:0){id name status}}"
}'
响应分页数据:
{
"data": {
"products": [
{
"id": "1",
"name": "T-Shirt",
"status": "Published"
},
...
]
}
}
获取产品及关联订单
GraphQL支持关联查询:
curl --request POST 'http://localhost:8081/graphql' \
--header 'Content-Type: application/json' \
--data \
'{
"query": "query {product(id:1){ id name orders{customerId address status creationDate}}}"
}'
单次请求获取产品和订单:
{
"data": {
"product": {
"id": "1",
"name": "T-Shirt",
"orders": [
{
"customerId": "de68a771-2fcc-4e6b-a05d-e30a8dd0d756",
"status": "Delivered",
"address": "43-F 12th Street",
"creationDate": "Mon Jan 17 01:00:18 GST 2022"
},
...
]
}
}
}
对比REST方案,GraphQL用一次请求解决了关联数据获取问题。
对比
示例展示了GraphQL如何减少客户端-服务器通信量,并允许客户端定制响应格式。虽然后端数据操作可能相同,但更丰富的接口让客户端更省力。
灵活性与动态性
GraphQL的优势:
- 客户端可按需请求字段
- 支持别名(Aliases)自定义返回键名
- 客户端可通过查询控制结果排序
- API变更对客户端影响更小,无需遵循固定响应结构
更高效的请求
每个服务器请求都有往返时间和载荷成本。
REST方案可能需要多次请求才能完成功能,且响应可能包含冗余数据。GraphQL则通常能用单次请求获取所有数据,简单粗暴地解决了效率问题。
何时选择REST?
GraphQL不是REST的替代品,两者甚至可以共存。GraphQL接口的额外复杂度是否值得,取决于具体场景。
选择REST的场景:
- 应用天然是资源驱动的,操作与资源实体直接关联
- 需要Web缓存(GraphQL原生不支持)
- 需要文件上传(GraphQL原生不支持)
总结
本文通过实际案例对比了REST和GraphQL。两种方案没有绝对优劣,选择应基于具体需求。有时两者共存也是合理方案。
示例代码可在GitHub获取。