【问题标题】:What’s the best RESTful method to return total number of items in an object?返回对象中项目总数的最佳 RESTful 方法是什么?
【发布时间】:2011-04-12 13:45:11
【问题描述】:

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

但是,通过我的 API 获取成员总数的最佳 RESTful 方式是什么?

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

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

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

【问题讨论】:

标签: rest restful-url


【解决方案1】:

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

标题

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

有一堆(标准和自定义)标头用于返回分页相关信息,包括总数。

X-总计数

X-Total-Count: 234

这个用在someAPIs我在野外发现的。还有NPM packages 用于添加对此标头的支持,例如环回。一些articles 建议也设置此标头。

它经常与Link header 结合使用,这是一个很好的分页解决方案,但缺少总计数信息。

链接

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

我觉得,通过阅读大量有关此主题的内容,普遍的共识是使用Link header 向使用rel=nextrel=previous 等的客户端提供分页链接。问题在于它缺少总共有多少条记录的信息,这就是为什么许多 API 将其与 X-Total-Count 标头结合在一起的原因。

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

内容范围

Content-Range: items 0-49/234

由名为Range header, I choose you (for pagination)! 的博客文章推广。作者为使用RangeContent-Range 标头进行分页提供了强有力的案例。当我们仔细阅读这些标头上的 the RFC 时,我们发现将它们的含义扩展到字节范围之外实际上是 RFC 预期的,并且是明确允许的。当在items 而不是bytes 的上下文中使用时,Range 标头实际上为我们提供了一种方法来请求特定范围的项目并指示响应项目涉及的总结果的范围。此标题还提供了一种显示总数的好方法。它是一个真正的标准,主要将一对一映射到分页。也是used in the wild

信封

包括the one from our favorite Q&A website 在内的许多API 都使用信封,这是一种数据包装器,用于添加有关数据的元信息。此外,ODataJsonApi 标准都使用响应信封。

这样做的最大缺点(恕我直言)是处理响应数据变得更加复杂,因为必须在信封中的某处找到实际数据。该信封还有许多不同的格式,您必须使用正确的格式。很明显,来自 OData 和 JsonApi 的响应信封大相径庭,OData 在响应中的多个点混合在元数据中。

单独的端点

我认为其他答案已经对此进行了足够的介绍。我没有进行太多调查,因为我同意 cmets 的观点,因为您现在有多种类型的端点,因此这很令人困惑。我认为最好每个端点都代表一个(集合)资源。

进一步的想法

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

【讨论】:

  • 我对 Range 标头特别感兴趣,但是我找不到足够的证据证明使用除 bytes 之外的任何东西作为范围类型是有效的。
  • 我认为最清楚的证据可以在section 14.5 of the RFC:acceptable-ranges = 1#range-unit | "none" 中找到,我认为这个公式明确地为bytes 之外的其他范围单位留出了空间,尽管规范本身只定义了bytes
【解决方案2】:

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

StackOverflow API 是相同设计的一个很好的例子。这是用户方法的文档 - https://api.stackexchange.com/docs/users

【讨论】:

  • +1:如果要实施提取限制,这绝对是最 RESTful 的做法。
  • @bzim 你会知道有一个下一页要获取,因为有一个带有 rel="next" 的链接。
  • @Donal “下一个” rel 已在 IANA iana.org/assignments/link-relations/link-relations.txt注册
  • @Darrel - 是的,可以使用有效负载中的任何类型的“下一个”标志来完成。我只是觉得响应中收集项目的总数本身很有价值,并且可以作为“下一个”标志。
  • 返回一个不是项目列表的对象不是 REST API 的正确实现,但 REST 不提供任何方法来获取部分结果列表。因此,为了尊重这一点,我认为我们应该使用标头来传输其他信息,例如总计、下一页令牌和上一页令牌。我从未尝试过,我需要其他开发人员的建议。
【解决方案3】:

我更喜欢将 HTTP 标头用于此类上下文信息。

对于元素的总数,我使用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

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

【讨论】:

  • RFC6648 不赞成使用字符串 X- 作为非标准化参数名称前缀的约定。
  • 我同意 JDawg 的观点。如果您愿意,请首选“Resource-Count”或“Total-Count”或“MyApp-Total-Count”以尊重 RFC6648
  • github 使用 x-total-count 是在哪里写的?我只能找到有关链接头的信息。
【解决方案4】:

不需要实际物品时的替代方案

Franci Penov's answer 无疑是最好的方法,因此您始终会返回项目以及有关所请求实体的所有其他元数据。应该这样做。

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

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

或类似的东西......

【讨论】:

  • 在标头中嵌入此信息的好处是您可以发出 HEAD 请求来获取计数。
  • @felixfbecker 确切地说,感谢您重新发明轮子并使用各种不同的机制使 API 变得混乱 :)
  • @EralpB 感谢您重新发明轮子并弄乱 API!? HEAD 在 HTTP 中指定。 metaonlyincludeitems 不是。
  • @felixfbecker 仅“完全”是为您准备的,其余的都是为 OP 准备的。很抱歉造成混乱。
  • REST 就是利用 HTTP 并尽可能多地利用它来达到预期目的。在这种情况下应使用 Content-Range (RFC7233)。体内的解决方案不好,尤其是因为它不适用于 HEAD。按照此处的建议创建新标题是不必要且错误的。
【解决方案5】:

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

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

【讨论】:

  • “自定义 HTTP 标头”?这将归于有点令人惊讶的标题,这反过来又与我认为的 RESTful API 应该是相反的。最终,这应该不足为奇。
  • @Donal 我知道。但是所有好的答案都已经被采纳了。 :(
  • 我也知道,但有时你只需要让其他人来回答。或者以其他方式使您的贡献更好,例如详细解释为什么应该以最好的方式而不是其他方式来完成。
  • 在受控环境中,这可能不足为奇,因为它可能会在内部使用并基于开发人员的 API 策略。我想说这在某些情况下是一个很好的解决方案,值得在这里作为一个可能不寻常的解决方案的说明。
  • 我非常喜欢在这种事情上使用 HTTP 标头(这确实是它所属的地方)。在这种情况下,标准 Link header 可能是合适的(Github API 使用它)。
【解决方案6】:

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

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

【讨论】:

    【解决方案7】:

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

    我们发现“Accept-Ranges”是映射分页范围的最佳选择:https://www.rfc-editor.org/rfc/rfc7233#section-2.3 因为“范围单位”可以是“字节”或“令牌”。两者都不代表自定义数据类型。 (见:https://www.rfc-editor.org/rfc/rfc7233#section-4.2) 不过,据说

    HTTP/1.1 实现可以忽略使用其他指定的范围 单位。

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

    这样,我们必须将 Accept-Ranges 设置为“成员”或任何我们期望的范围单位类型。此外,还将 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:

    如果所有前提条件都为真,则服务器支持范围
    目标资源的标头字段,以及指定的范围是
    有效且可满足(如第 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
    

    【讨论】:

      【解决方案8】:

      一个新的端点怎么样 > /api/members/count 只是调用 Members.Count() 并返回结果

      【讨论】:

      • 给计数一个明确的端点使它成为一个独立的可寻址资源。它会起作用,但会为您的 API 新手提出有趣的问题 - 集合成员的计数是否是集合中的单独资源?我可以用 PUT 请求更新它吗?它是针对空集合存在还是仅存在于其中?如果members 集合可以通过对/api 的POST 请求来创建,那么/api/members/count 是否也会被创建为副作用,或者我必须在请求之前执行显式的POST 请求来创建它吗? :-)
      【解决方案9】:

      似乎最容易添加一个

      GET
      /api/members/count
      

      并返回成员总数

      【讨论】:

      • 不是一个好主意。您有义务让客户提出 2 次请求以在其页面上构建分页。第一个请求获取资源列表,第二个请求计算总数。
      • 我认为这是一个好方法......你也可以只返回结果列表作为 json 并在客户端检查集合的大小,所以这种情况是愚蠢的例子......而且你可以有 /api /members/count 然后 /api/members?offset=10&limit=20
      • 还要记住,很多分页类型不需要计数(比如无限滚动)——为什么在客户端可能不需要的时候计算这个
      • 它也可能导致某些路由器出现问题,因为/api/members/:memberId 路由模式也会匹配此问题,因此实际上您可能会尝试在数据库中查询where id = 'count'。您可以更改路由的顺序或执行其他解决方法,或者通过为 :memberId 路由参数指定正则表达式来使其工作,但无论如何它可能会令人困惑
      【解决方案10】:

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

      P。 S. 实际上你可以用 $resource/AngularJS 做到这一点,但它需要一些调整。

      【讨论】:

      • 这些调整是什么?他们会对这样的问题有所帮助:stackoverflow.com/questions/19140017/…
      • Angular 不需要数组作为查询结果,您只需使用选项对象属性配置资源:isArray: false|true
      【解决方案11】:

      您可以将counts 视为一种资源。该 URL 将是:

      /api/counts/member
      

      【讨论】:

        【解决方案12】:

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

        作为 API 使用者,我希望能够表示每个计数值 或者作为可数资源的子资源(即 GET /tasks/count 用于任务计数),或作为更大的字段 与相关资源相关的元数据聚合(即 GET /任务/元数据)。通过在同一父级下确定相关端点的范围 资源(即 /tasks),API 变得直观,并且 可以(通常)从其路径和 HTTP 方法推断端点。

        其他想法:

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

        【讨论】:

          【解决方案13】:

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

          我更喜欢有单独的 count 端点(或带有参数 countOnly 的相同端点)。因为您可以通过显示正确启动的进度条来为最终用户准备长时间/耗时的过程。

          如果你想在每个响应中返回datasize,应该有pageSize,offset也提到。老实说,最好的方法也是重复请求过滤器。但反应变得非常复杂。所以,我更喜欢专用端点来返回计数。

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

          我的 Couleage,更喜欢 countOnly 参数而不是现有端点。因此,当指定时,响应仅包含元数据。

          endpoint?filter=value

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

          endpoint?filter=value&countOnly=true

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-10-23
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2010-11-08
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多