概述

在构建应用系统的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 
}

获取产品及关联订单

要获取产品和订单,通常需要两步:

  1. 先获取产品列表
  2. 再调用订单接口查询关联数据:
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获取。


原始标题:GraphQL vs REST