返回一个对象中总项数的最佳 RESTful 方法是什么?

我正在为我参与的一个大型社交网站开发一个 REST API 服务。目前为止,效果很好。我可以发出 GETPOSTPUTDELETE请求对象 URL 和影响我的数据。但是,此数据是分页的(每次限制为30个结果)。

什么是最好的 RESTful 方式来获得总数,比如说,成员,通过我的 API?

目前,我向 URL 结构发出如下请求:

  • /api/member -返回一个成员列表(如上所述,每次30个)
  • 影响单个成员,具体取决于使用的请求方法

我的问题是: 如何使用类似的 URL 结构来获得应用程序中的成员总数?显然,只请求 id字段(类似于 Facebook 的 Graph API)并计算结果是无效的,因为只会返回30个结果中的一部分。

187444 次浏览

如果有一个新的端点 >/api/member/Count,它只调用 Member.Count ()并返回结果

看起来最简单的方法就是加一个

GET
/api/members/count

并返回成员总数

虽然对/API/users 的响应是分页的,并且只返回30条记录,但是没有什么能阻止您在响应中包含记录总数和其他相关信息,如页面大小、页码/偏移量等。

StackOverflow API 是同样设计的一个很好的例子

您可以将计数作为响应 HEAD 请求的自定义 HTTP 标头返回。这样,如果客户端只需要计数,就不需要返回实际的列表,也不需要额外的 URL。

(或者,如果您处于一个从一个端点到另一个端点的受控环境中,您可以使用一个自定义 HTTP 谓词,如 COUNT。)

有时候,框架(如 $resource/AngularJS)需要一个数组作为查询结果,在这种情况下,不能真正使用像 {count:10,items:[...]}这样的响应,我将“ count”存储在 response Header 中。

实际上你可以使用 $resource/AngularJS 来实现,但是它需要一些调整。

当请求分页数据时,您知道(通过显式的页面大小参数值或默认的页面大小值)页面大小,所以您知道是否得到了响应的所有数据。当响应的数据少于页面大小时,就得到了整个数据。当返回一个完整的页面时,您必须再次请求另一个页面。

我更喜欢使用单独的端点来计数(或者使用参数 CountOnly 设置相同的端点)。因为您可以通过显示正确启动的进度条来为最终用户准备长时间的流程。

如果希望在每个响应中返回数据大小,则应该提到 pageSize 和偏移量。老实说,最好的方法也是重复一个请求过滤器。但反应变得非常复杂。因此,我更喜欢专用端点来返回计数。

<data>
<originalRequest>
<filter/>
<filter/>
</originalReqeust>
<totalRecordCount/>
<pageSize/>
<offset/>
<list>
<item/>
<item/>
</list>
</data>

与现有端点相比,Couleage 更喜欢使用 CountOnly 参数。

过滤器 = 值

<data>
<count/>
<list>
<item/>
...
</list>
</data>

Filter = value & CountOnly = true

<data>
<count/>
<!-- empty list -->
<list/>
</data>

当你不需要实际的物品时可以选择

Franci Penov 的回答 当然是最好的方法,因此您总是返回项目以及所有关于被请求实体的附加元数据。这才是应该做的。

但有时返回所有数据是没有意义的,因为您可能根本不需要它们。也许您所需要的只是关于所请求资源的元数据。比如总数,页数或者其他什么。在这种情况下,你可以让 URL 查询告诉你的服务不要返回条目,而只是元数据,比如:

/api/members?metaonly=true
/api/members?includeitems=0

或者类似的东西。

对于这种上下文信息,我更喜欢使用 HTTP Header。

对于元素的总数,我使用 X-total-count头。
对于指向下一页、上一页等的链接,我使用 HTTP Link头:
Http://www.w3.org/wiki/linkheader

Github 也是这样做的: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#pagination

在我看来,它更干净,因为它也可以用于当您返回的内容不支持超链接(即二进制文件,图片)。

我建议添加相同的标题,比如:

HTTP/1.1 200


Pagination-Count: 100
Pagination-Page: 5
Pagination-Limit: 20
Content-Type: application/json


[
{
"id": 10,
"name": "shirt",
"color": "red",
"price": "$23"
},
{
"id": 11,
"name": "shirt",
"color": "blue",
"price": "$25"
}
]

详情请参阅:

Https://github.com/adnan-kamili/rest-api-response-format

对于大摇大摆的文件:

Https://github.com/adnan-kamili/swagger-response-template

最近,我对这个问题和其他与 REST 分页相关的问题做了一些广泛的研究,认为在这里添加一些我的发现是很有建设性的。我正在扩展这个问题,以包括对分页和计数的想法,因为它们是密切相关的。

标题

分页元数据以响应头的形式包含在响应中。这种方法的最大好处是响应有效负载本身就是实际的数据请求者所要求的。使对分页信息不感兴趣的客户端更容易处理响应。

在通用环境中,有许多(标准和自定义)标头用于返回与分页相关的信息,包括总计数。

X 总计

X-Total-Count: 234

这是用于 一些 应用程式界面我在野外发现。还有 NPM 软件包用于添加对这个头的支持,例如 Loopback。一些 物品也建议设置这个头。

它通常与 Link头结合使用,这是一个非常好的分页解决方案,但是缺乏总计数信息。

林克

Link: </TheBook/chapter2>;
rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
</TheBook/chapter4>;
rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

我觉得,从阅读了很多关于这个主题,一般的共识是使用 Link提供分页链接到客户使用 rel=nextrel=previous等。这样做的问题在于,它缺乏总共有多少条记录的信息,这就是为什么许多 API 将其与 X-Total-Count头结合在一起的原因。

或者,一些 API,例如 JsonApi标准,使用 Link格式,但是将信息添加到响应信封中而不是标题中。这简化了对元数据的访问(并创建了一个添加总计数信息的位置) ,但是增加了访问实际数据本身的复杂性(通过添加信封)。

内容范围

Content-Range: items 0-49/234

由一篇名为 范围标头,我选择你(分页) !的博客文章推广。作者强烈建议使用 RangeContent-Range标头进行分页。当我们仔细阅读这些头上的 RFC时,我们发现将它们的含义扩展到字节范围之外实际上是 RFC 所预期的,并且是明确允许的。当在 items而不是 bytes的上下文中使用时,Range 头实际上为我们提供了一种方法,既可以请求一定范围的条目,又可以指示响应条目所涉及的总结果的范围。这个头部也提供了一个很好的方式来显示总计数。它是一个真正的标准,主要是将一对一映射到分页。它也是 在野外使用

信封

包括 我们最喜欢的问答网站上的那个在内的许多 API 都使用 信封,它是数据的包装器,用于添加有关数据的元信息。此外,ODataJsonApi标准都使用响应信封。

这种方法(imho)的最大缺点是处理响应数据变得更加复杂,因为实际数据必须在信封中的某个地方找到。此外,该信封有许多不同的格式,你必须使用正确的一个。OData 和 JsonApi 的响应信封截然不同,OData 在响应的多个点混合了元数据。

单独的端点

我觉得其他答案已经说得够多了。我没有进行这么多的研究,因为我同意这样的评论,即这是令人困惑的,因为您现在有多种类型的端点。我认为,如果每个端点代表一个(集合)资源,那将是最好的。

进一步的想法

我们不仅需要传递与响应相关的分页元信息,而且还允许客户端请求特定的页面/范围。同样有趣的是,考察这个方面,最终得出一个连贯的解决方案。这里我们也可以使用 Header (Range Header 似乎非常合适) ,或者其他机制,例如查询参数。有些人主张将结果页面作为单独的资源处理,这在某些用例中可能是有意义的(例如 /books/231/pages/52)。除了支持 Range头(以及作为请求参数)之外,我还选择了一系列经常使用的请求参数,如 pagesizepage[size]limit等。

从“ X-”开始,前缀已被弃用(参见: https://www.rfc-editor.org/rfc/rfc6648)

我们发现“ Accept-Range”是映射分页范围的最佳选择: (a href = “ https://www.rfc-edit or.org/rfc/rfc7233 # section-2.3”rel = “ nofollow norefrer”> https://www.rfc-editor.org/rfc/rfc7233#section-2.3 因为“范围单位”可以是“字节”或“令牌”。两者都不表示自定义数据类型。(参见: https://www.rfc-editor.org/rfc/rfc7233#section-4.2) 尽管如此,据说

HTTP/1.1实现可能忽略使用 other 单位。

这表明: 使用自定义范围单位不违反协议,但它可能被忽略。

这样,我们必须将 Accept-Range 设置为“成员”或者我们期望的任何远程单位类型。此外,还要将 Content-Range 设置为当前范围。(见: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12)

无论如何,我会坚持 RFC7233(https://www.rfc-editor.org/rfc/rfc7233#page-8)的建议,发送一个206而不是200:

如果所有前提条件均为真,则服务器支持 Range
目标资源的头字段,指定的范围为
有效和可满足的(如第2.1节定义) ,服务器应该
发送一个206(部分内容)响应,其有效载荷包含一个
或对应于可满足的
要求的范围,如第4节所定义。

因此,我们将得到以下 HTTP 头字段:

部分内容:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

完整内容:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20

你可以考虑将 counts作为一个资源,然后 URL 将是:

/api/counts/member

关于设计用于返回多个对象计数的 REST API 的有趣讨论: Https://groups.google.com/g/api-craft/c/qbi2qrrpfew/m/h30dynrqewaj?pli=1

作为一个 API 使用者,我希望每个计数值都被表示出来 作为可数资源(即 GET)的子资源 任务/计算任务的数量) ,或者作为一个更大的 与相关资源相关的元数据的聚合(即 GET 通过在同一父节点下确定相关端点的范围 资源(即/任务) ,API 变得直观,和目的 端点(通常)可以从它的路径和 HTTP 方法中推断出来。

其他想法:

  1. 如果每个单独的计数只在与其他计数(例如,对于统计数据仪表板)的组合中有用,那么您可以 公开一个端点,该端点聚合并返回所有计数 一次。
  2. 如果您有一个用于列出所有资源的现有端点(即用于列出所有任务的 GET/任务) ,则可以将计数包含在 响应作为元数据,或者作为 HTTP 标头,或者作为响应体中的元数据。 这样做将对 API 产生不必要的负载,这可能是 根据你的用例可以忽略不计。

鉴于“ X-”前缀已被弃用,以下是我得出的结论:

  1. 在响应中增加了另一个项目计数: 23
  2. 在应用程序中使用数据之前从响应中删除该项。

如果资源计数为 资源元数据 = > new 端点

如果资源计数(可能还有其他元数据)对客户机应用程序和最终用户有用,那么它不需要位于头部; 它可以合法地位于响应主体中。可以存在与给定资源相关的其他元数据(其他总数、平均值或统计数据)。

在这种情况下,我将使用 特定端点作为资源元数据。回到最初问题中的社交网络成员的例子,可能有:

GET /api/members         // collection of members, which could be paginated
GET /api/members/{id}    // a single member
GET /api/members/stats   // total member count; member count per region; average posts per member; etc.

stats不同,您可能更喜欢 metadatatotals或端点中的其他东西(YMMV)。但是,我们将此端点视为返回某些特定于 成员资源的内容。这就是为什么它是一个单独的端点而不是查询字符串参数的原因。类似地,我们应该有一个单独的端点——而不是一个查询参数——用于一个成员的帖子:

GET /api/members/{id}/posts

如果资源计数是为 分页 = > 使用标头

如果客户端应用程序正在处理 GET 请求中为分页目的返回的资源数量,那么这个资源计数就是 信息元数据。

在这种情况下,我同意使用标头是一种更好的方法。你应该看看 Stijn de WittAdnan Kamili的答案。