S1E5 如何设计一个符合 RESTFul 风格的批量操作的 OpenAPI 接口?
许久不见,甚是想念,最近做了不少 RESTFul API 的设计实践,分享一下最新的经验。
在遵循 RESTFul 风格设计 OpenAPI 时,简单的创建、更新、查询、删除是比较好做的,大家也大多能够想到如何设计对应的接口,无非是用 POST 做创建、GET 做查询、DELETE 做删除、PUT、PATCH 做更新,但设计到批量的操作时,大家往往会有一些困惑,今天我们就来聊聊 RESTFul 下的批量操作。在给出我自己具体的实践之前,先来看看业界的实践。
业界的两种实践
由于 RESTFul 风格定义当中并没有明确说明如何设计批量接口,所以业界上也有不同的批量接口设计的思路,大体可以分为两种:
- 在网关层面设计批量接口的能力,实现异步转同步:这部分主要是会提供一个单独的请求路径,由开发者将自己的请求打包并发送到这个新的路径,由这个路径上的服务将请求结果发送给具体的服务,并将结果收回,返回给开发者。Google 和 Microsoft Graph 就提供了这样的方式。
- 实现 Bulk 资源作为异步任务,开发者发起请求后自行查询任务状态:这部分就类似于上面的设计,比较典型的是 ZenDesk 的 OpenAPI,大量使用类似的设计。
前者依赖有较强的网关研发能力,本质上是把业务上的复杂性前置,移动到了网关层面来实现,从而降低对于业务方的压力。但这个用法比较适合像 Google、微软这样的巨型公司,内部业务线林立,重复建设存在比较高的成本,所以抽象出方案交给中台来做比较合适。接下来,我们来看看我自己的实践经验
我自己的实践经验
因为这篇文章是给那些 RESTFul 的开发者准备的,所以这里我也按照 RESTFul 的标准方法的设计思路,设计如下的 RESTFul 风格 API。
批量获取 – 搜索视角和列表视角
在 RESTFul 接口时,我们一般会提供 List 接口,用于查询某个资源的所有数据,获取单个用户和用户列表的接口如下:
GET /users #获取用户列表
GET /users/:id #获取特定的用户
而我们的诉求是获取多个用户时,可以根据实际的场景,来决定具体提供哪一类接口。
一般来说,批量获取有两种不同的场景,所以这里我们提供两种不同的 API 设计思路,来解决这两种不同场景下的使用习惯。当然,你也可以使用其中一种,并配合一部分客户端的操作,来实现另外一种,这里就不做详细的解释。
列表场景
如果你的业务场景,是从众多数据中获取特定的数据,且可以接受不设定参数时返回所有数据,那么我推荐你通过在获取列表的方法当中,增加筛选器的方式来实现。
假设我们要实现基于 ID 获取多个用户信息,则可以在获取用户列表上稍作调整来实现。
获取所有用户
# request
GET /users #获取所有用户
# response
HTTP/1.1 200 OK
{
"code":0,
"data":[
{
...
},
...
]
}
获取 ID=1 以及 ID = 2 的 用户
# request
GET /users?id[]=1&id[]=2 # 获取所有用户中 ID=1 和 ID=2 的用户
# response
HTTP/1.1 200 OK
{
"code":0,
"data":[
{
...
},
...
]
}
对于开发者来说,可以继续沿用同一个接口来实现能力,并且可以配合其他的 Filter 一同实现筛选。
搜索场景
而批量获取数据除了偏普适性质的列表场景,还有一类是明确只需要获取特定的数据,则可以从搜索绕道,来实现批量获取的能力。
批量获取用户数据
# request
POST /search?type=user
{
"query":"id=1 or id=2"
}
# response
HTTP/1.1 200 OK
{
"code":0,
"data":[
{
...
},
...
]
}
搜索接口的好处是可承载数据极大,不受限制(HTTP 的 Query 长度受限于 URL 协议总长度,Chrome 是 2MB),同时,在用户不输入任何内容时,不返回任何结果,适合用在开发搜索等业务场景。
批量创建、批量更新、批量删除
批量获取说完了,接下来我们来聊聊批量更新,实际上批量更新、批量创建虽然有场景,但也不多,在这种场景下,我们已经很难像批量获取那样,在原有资源上进行操作,而是需要借助批量资源来实现批量操作。
以用户资源(User)为例,当我们需要对其进行批量创建、删除、更新时,我们需要创建一个批量资源 BulkUser,并通过对 BulkUser 操作,来创建用户。Bulk User 本质上是将请求的多个资源转换为了异步的任务,在发起后,开发者可以在任务结果中查询具体的值来使用。
这里异步的任务更多是一种设计表现,并不强制要求一定异步。异步的表现设计和相关的接口实现,是为了给后续留出纵向扩展的空间。既 无论是否行为是否真实异步,都需要在返回结果中返回任务 ID & 任务状态,以便于开发者自行实现异步处理的逻辑。
{
"code":0,
"data":{
"job":{
"id":"123",
"status":"ok"
},
"results":[
{
...
}
]
}
}
批量创建
批量创建用户的操作和创建单个用户的操作是比较接近的,主要差异点在于 Path 上有区别,且传递参数时,会传递多个资源的属性。
批量创建用户
# request
POST /bulk_users/
{
"users":[
{
// user1
...
},
{
// user2
...
}
]
}
# response
HTTP/1.1 200 OK
{
"code":0,
"data":[
{
// user1
...
},
{
// user2
...
}
]
}
更新用户
批量更新时,你已经知道了你需要更新的资源的 ID,因此,可以这样设计的你的接口:
#request
PUT /bulk_users?id[]=1&id[]=2
{
"gender":"other"
}
# response
HTTP/1.1 204 No Content
{
"code":0,
"data":[
{
// id=1
...
},
{
// id=2
...
}
]
}
批量删除
有了上面的几个例子,批量删除就比较好定义了。就像这样:
#request
DELETE /bulk_users?id[]=1&id[]=2
# response
HTTP/1.1 200 OK
{
"code":0,
"data":[
{
// id=1
"status":"deleted"
},
{
// id=2
"status":"deleted"
}
]
}
总结
批量操作在获取场景,可以考虑通过 List + Filter 的方式,或搜索的方式来实现一套更加标准的搜索接口,而规避提供定制化的自定义接口。从规范的视角,两者都是符合规范的,也可以都对用户提供,并不互斥。而对于没办法复用的创建、更新、删除,则可以考虑使用创建异步任务的方式,来实现批量操作,给开发者一个明确的异步预期,让开发者可以自行查询业务的实现方式。