S1E5 如何设计一个符合 RESTFul 风格的批量操作的 OpenAPI 接口?

S1E5 如何设计一个符合 RESTFul 风格的批量操作的 OpenAPI 接口?
Photo by Caspar Camille Rubin / Unsplash

许久不见,甚是想念,最近做了不少 RESTFul  API 的设计实践,分享一下最新的经验。


在遵循 RESTFul 风格设计 OpenAPI 时,简单的创建、更新、查询、删除是比较好做的,大家也大多能够想到如何设计对应的接口,无非是用 POST 做创建、GET 做查询、DELETE 做删除、PUT、PATCH 做更新,但设计到批量的操作时,大家往往会有一些困惑,今天我们就来聊聊 RESTFul 下的批量操作。在给出我自己具体的实践之前,先来看看业界的实践。

业界的两种实践

由于 RESTFul 风格定义当中并没有明确说明如何设计批量接口,所以业界上也有不同的批量接口设计的思路,大体可以分为两种:

  1. 在网关层面设计批量接口的能力,实现异步转同步:这部分主要是会提供一个单独的请求路径,由开发者将自己的请求打包并发送到这个新的路径,由这个路径上的服务将请求结果发送给具体的服务,并将结果收回,返回给开发者。Google 和 Microsoft Graph 就提供了这样的方式。
  2. 实现 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 的方式,或搜索的方式来实现一套更加标准的搜索接口,而规避提供定制化的自定义接口。从规范的视角,两者都是符合规范的,也可以都对用户提供,并不互斥。而对于没办法复用的创建、更新、删除,则可以考虑使用创建异步任务的方式,来实现批量操作,给开发者一个明确的异步预期,让开发者可以自行查询业务的实现方式。

Read more

加更: 聊聊 APILetter 的新计划

加更: 聊聊 APILetter 的新计划

Hi,你好, APILetter 从创刊号,到 S1E6,经历了一年的时间。 虽然在定更新节奏时,我就考虑到自己拖更的可能性,但确实没想到我拖更这么严重,在 2022 年,一口气更新了 3 篇,然后就是长达半年的拖更。不过,总算是把第六篇写完,算是给 Season 1 做个了结。 过去 APILetter 的出现,是源自我在研究 RESTFul 架构时发现的问题:国内有太多解释什么是 RESTFul 规范的文章,但你点进去看,篇篇都是复制粘贴。 而 API 是开发者生态中非常重要的一环,它不应该被草率的对待,开发者们值得用上更好的 API。既然没有人写关于 API 的严肃内容,那就从我开始吧。刚好我在研究相关的内容,那就写一些 API 到底应该是什么样的。 也正是抱着这个想法,我开始了一篇篇的创作,

By 白宦成