【问题标题】:Pagination response payload from a RESTful API来自 RESTful API 的分页响应负载
【发布时间】:2012-08-23 12:19:50
【问题描述】:

我想在我的 RESTful API 中支持分页。

我的 API 方法应该通过 /products/index 返回一个 JSON 产品列表。但是,可能有数千种产品,我想对它们进行分页,所以我的请求应该如下所示:

/products/index?page_number=5&page_size=20

但是我的 JSON 响应需要是什么样的? API 使用者通常会期望响应中的分页元数据吗?还是只需要一系列产品?为什么?

看起来 Twitter 的 API 包含元数据:https://dev.twitter.com/docs/api/1/get/lists/members(请参阅示例请求)。

使用元数据:

{
  "page_number": 5,
  "page_size": 20,
  "total_record_count": 521,
  "records": [
    {
      "id": 1,
      "name": "Widget #1"
    },
    {
      "id": 2,
      "name": "Widget #2"
    },
    {
      "id": 3,
      "name": "Widget #3"
    }
  ]
}

只是一个产品数组(没有元数据):

[
  {
    "id": 1,
    "name": "Widget #1"
  },
  {
    "id": 2,
    "name": "Widget #2"
  },
  {
    "id": 3,
    "name": "Widget #3"
  }
]

【问题讨论】:

    标签: rest pagination


    【解决方案1】:

    ReSTful API 主要由其他系统使用,这就是我将分页数据放在响应标头中的原因。但是,一些 API 使用者可能无法直接访问响应标头,或者可能正在您的 API 上构建 UX,因此提供一种方法来检索(按需)JSON 响应中的元数据是一个优点。

    我相信您的实现应该默认包含机器可读的元数据,并在请求时包含人类可读的元数据。如果您愿意,可以随每个请求返回人类可读的元数据,或者最好通过查询参数按需返回,例如include=metadatainclude_metadata=true

    在您的特定场景中,我会在记录中包含每个产品的 URI。这使 API 使用者可以轻松地创建指向各个产品的链接。我还会根据我的寻呼请求的限制设置一些合理的期望。实施和记录页面大小的默认设置是一种可接受的做法。例如,GitHub's API 将默认页面大小设置为 30 条记录,最多 100 条记录,并设置您可以查询 API 的次数的速率限制。如果您的 API 具有默认页面大小,则查询字符串可以只指定页面索引。

    在人类可读的场景中,当导航到 /products?page=5&per_page=20&include=metadata 时,响应可能是:

    {
      "_metadata": 
      {
          "page": 5,
          "per_page": 20,
          "page_count": 20,
          "total_count": 521,
          "Links": [
            {"self": "/products?page=5&per_page=20"},
            {"first": "/products?page=0&per_page=20"},
            {"previous": "/products?page=4&per_page=20"},
            {"next": "/products?page=6&per_page=20"},
            {"last": "/products?page=26&per_page=20"},
          ]
      },
      "records": [
        {
          "id": 1,
          "name": "Widget #1",
          "uri": "/products/1"
        },
        {
          "id": 2,
          "name": "Widget #2",
          "uri": "/products/2"
        },
        {
          "id": 3,
          "name": "Widget #3",
          "uri": "/products/3"
        }
      ]
    }
    

    对于机器可读的元数据,我会在响应中添加Link headers

    Link: </products?page=5&perPage=20>;rel=self,</products?page=0&perPage=20>;rel=first,</products?page=4&perPage=20>;rel=previous,</products?page=6&perPage=20>;rel=next,</products?page=26&perPage=20>;rel=last
    

    (链接头的值应该是urlencoded)

    ...如果您愿意,也可能是自定义 total-count 响应标头:

    total-count: 521
    

    以人为中心的元数据中显示的其他分页数据对于以机器为中心的元数据可能是多余的,因为链接标题让我知道我在哪个页面以及每页的数量,并且我可以快速检索记录数在数组中。因此,我可能只会为总数创建一个标题。您可以随时改变主意并添加更多元数据。

    顺便说一句,您可能会注意到我从您的 URI 中删除了 /index。一个普遍接受的约定是让您的 ReST 端点公开集合。最后加上/index 会使情况稍微混乱。

    这些只是我在使用/创建 API 时喜欢的一些东西。希望对您有所帮助!

    【讨论】:

    • per_page 不遵循 page_size 约定
    • "page_count": 20{"last": "/products?page=26&amp;per_page=20"}?
    • 如果在从第 1 页到第 x 页获取所有记录时产品的计数突然增加会发生什么?
    • @MeV 与任何基于光标的分页场景中发生的情况相同:总数会增加,如果最后一页已满,页数可能会增加,仅此而已。这是每个使用这种分页类型的应用程序的一个非常常见的场景。如果新产品出现在第一页或最后一页,这将取决于所使用的排序方式。
    • “ReSTful API 主要由其他系统使用,这就是我将分页数据放在响应标头中的原因” 就像说外面阳光明媚,这就是我为什么穿着蓝色衬衫。是什么让您认为标题无法被人类阅读?
    【解决方案2】:

    作为一个为使用 REST 服务编写了多个库的人,让我从客户的角度来说明为什么我认为将结果包装在元数据中是可行的方法:

    • 没有总计数,客户端怎么知道它还没有收到所有的东西,应该继续对结果集进行分页?在不执行向前看下一页的 UI 中,在最坏的情况下,这可能表示为实际上没有获取更多数据的 Next/More 链接。
    • 在响应中包含元数据允许客户端跟踪较少的状态。现在我不必将我的 REST 请求与响应匹配,因为响应包含重建请求状态所需的元数据(在本例中是光标进入数据集)。
    • 如果状态是响应的一部分,我可以同时对同一个数据集执行多个请求,并且我可以按照它们碰巧到达的任何顺序处理请求,不一定是我发出请求的顺序。

    还有一个建议:像 Twitter API 一样,您应该将 page_number 替换为直接索引/光标。原因是,API 允许客户端根据请求设置页面大小。返回的 page_number 是客户端到目前为止请求的页数,还是给定最后使用的 page_size 的页数(几乎可以肯定是后者,但为什么不完全避免这种歧义)?

    【讨论】:

    • 对于您的第一个项目符号,如果没有下一页,省略 rel=next 链接是否是一个合适的解决方案?对于您的第二个项目符号,信息在对客户端的响应中仍然可用,只是不在响应的正文中,而是在标头中。为您的最后一段 +1。
    • 我想在最后回答你的建议。光标分页与页面/偏移分页不同。两者都有各自的优缺点和性能方面的考虑。
    【解决方案3】:

    我建议添加相同的标题。将元数据移动到标头有助于摆脱 resultdatarecords 之类的信封,并且响应正文仅包含我们需要的数据。如果您也生成分页链接,则可以使用Link 标头。

        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

    【讨论】:

    • 根据 RFC-6648,应在元数据键中删除“X-”前缀。
    • @RayKoopa 谢谢,我已经更新了 github 页面但忘记更新这个答案了。
    • 有趣的@adnankamili !如果由于某种原因,我们需要添加分页元数据和标题,你会怎么做?
    • 如果您已经有一个 API,其中数据还没有在信封中,这将特别有用,这样您就不必更改每个端点。元数据也是标题的用途,所以对我来说它在语义上很有意义。
    【解决方案4】:

    只需将您的后端 API 新属性添加到响应正文中即可。 来自示例 .net 核心:

    [Authorize]
    [HttpGet]
    public async Task<IActionResult> GetUsers([FromQuery]UserParams userParams)
    {
      var users = await _repo.GetUsers(userParams);
      var usersToReturn = _mapper.Map<IEnumerable<UserForListDto>>(users);
    
    
      // create new object and add into it total count param etc
      var UsersListResult = new
      {
        usersToReturn,
        currentPage = users.CurrentPage,
        pageSize = users.PageSize,
        totalCount = users.TotalCount,
        totalPages = users.TotalPages
      };
    
      return Ok(UsersListResult);
    }
    

    In body response it look like this

    {
    "usersToReturn": [
        {
            "userId": 1,
            "username": "nancycaldwell@conjurica.com",
            "firstName": "Joann",
            "lastName": "Wilson",
            "city": "Armstrong",
            "phoneNumber": "+1 (893) 515-2172"
        },
        {
            "userId": 2,
            "username": "zelmasheppard@conjurica.com",
            "firstName": "Booth",
            "lastName": "Drake",
            "city": "Franks",
            "phoneNumber": "+1 (800) 493-2168"
        }
    ],
    // metadata to pars in client side
    "currentPage": 1,
    "pageSize": 2,
    "totalCount": 87,
    "totalPages": 44
    

    }

    【讨论】:

      【解决方案5】:

      通常,我通过简单的方式创建一个restAPI端点,例如“localhost/api/method/:lastIdObtained/:countDateToReturn” 使用这些参数,您可以完成一个简单的请求。 在服务中,例如。 .net

      jsonData function(lastIdObtained,countDatetoReturn){
      '... write your code as you wish..'
      and into select query make a filter
      select top countDatetoreturn tt.id,tt.desc
       from tbANyThing tt
      where id > lastIdObtained
      order by id
      }
      

      在Ionic中,当我从下往上滚动时,我传递零值,当我得到答案时,我设置最后获得的id的值,当我从上到下滑动时,我传递最后一次注册我得到了身份证

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-09-06
        • 1970-01-01
        • 2015-04-30
        • 2018-11-18
        • 1970-01-01
        • 2016-08-06
        相关资源
        最近更新 更多