REST Web 应用程序中的分页
Posted
技术标签:
【中文标题】REST Web 应用程序中的分页【英文标题】:Pagination in a REST web application 【发布时间】:2010-10-21 01:24:53 【问题描述】:这是对this question 的更通用的重新表述(消除了 Rails 特定的部分)
我不确定如何在 RESTful Web 应用程序中的资源上实现分页。
假设我有一个名为 products
的资源,您认为以下哪种方法是最好的方法,为什么:
1。仅使用查询字符串
例如。 http://application/products?page=2&sort_by=date&sort_how=asc
这里的问题是我不能使用整页缓存,而且 URL 不是很干净,也不是很容易记住。
2。使用页面作为资源和查询字符串进行排序
例如。 http://application/products/page/2?sort_by=date&sort_how=asc
在这种情况下,看到的问题是http://application/products/pages/1
不是唯一资源,因为使用sort_by=price
会产生完全不同的结果并且我仍然不能使用页面缓存。
3。使用页面作为资源和 URL 段进行排序
例如。 http://application/products/by-date/page/2
我个人认为使用这种方法没有问题,但有人警告我这不是一个好方法(他没有给出理由,所以如果你知道为什么不推荐,请告诉我)
任何建议、意见、批评都非常受欢迎。谢谢。
【问题讨论】:
这是一个很好的问题。 额外问题:人们通常如何指定页面大小? 不要忘记矩阵参数w3.org/DesignIssues/MatrixURIs.html 【参考方案1】:我使用以下模式来获取下一页记录。 http://application/products?lastRecordKey=?&pageSize=20&sort=ASC
RecordKey 是在 DB 中保存顺序值的表的列。这用于从 DB 一次仅获取一页数据。 pageSize 用于确定要获取多少条记录。 sort 用于对记录进行升序或降序排序。
【讨论】:
【参考方案2】:我更喜欢使用查询参数 offset 和 limit。
offset:用于集合中项目的索引。
limit:用于项目数。
客户端可以简单地不断更新偏移量,如下所示
offset = offset + limit
下一页。
路径被视为资源标识符。并且页面不是资源,而是资源集合的子集。由于分页通常是 GET 请求,因此查询参数最适合分页而不是标题。
参考:https://metamug.com/article/rest-api-developers-dilemma.html#Requesting-the-next-page
【讨论】:
【参考方案3】:我同意 Fionn 的观点,但我会进一步说,对我来说,页面不是资源,它是请求的属性。这让我只选择了选项 1 查询字符串。感觉刚刚好。我真的很喜欢Twitter API 的结构安详。不太简单,不太复杂,有据可查。不管是好是坏,当我在做某事时,我的“首选”设计是一种方式而不是另一种方式。
【讨论】:
+1:查询字符串不是一等资源标识符;他们只是澄清了资源的排序和分组。 @S.Lott 请求是资源。您所谓的“一流资源”在section 5.2.1.1 of his dissertation 中由 Fielding 定义为 值。此外,在同一部分中,Fielding 将源代码文件的最新版本 作为资源的示例。这怎么可能是一个资源,但 最新的 10 个产品 是“产品资源请求的属性”?我知道您的观点更实用,但我认为它不太 RESTful。 请注意,我的评论并不意味着我不同意在 URL 上使用查询字符串的选择:只要 API 是超媒体驱动的,两者都是可行的解决方案,正如@RichApodaca 在他的回答中提到的那样.我只是指出,从 REST 的角度来看,页面应该被视为一种资源。【参考方案4】:寻找我在这个网站上遇到的最佳做法:
http://www.restapitutorial.com
在资源页面中有一个下载 .pdf 的链接,其中包含作者建议的完整 REST 最佳实践。其中有一个关于分页的部分。
作者建议添加对使用 Range 标头和使用查询字符串参数的支持。
请求
HTTP 标头示例:
Range: items=0-24
查询字符串参数示例:
GET http://api.example.com/resources?offset=0&limit=25
其中 offset 是开始项目编号,limit 是要返回的最大项目数。
回应
响应应包含一个 Content-Range 标头,指示正在返回的项目数以及尚待检索的总项目数
HTTP 标头示例:
Content-Range: items 0-24/66
Content-Range: items 40-65/*
在 .pdf 中还有一些针对更具体案例的其他建议。
【讨论】:
【参考方案5】:我在我的项目中使用以下网址:
http://application/products?page=2&sort=+field1-field2
这意味着 - “给我页面的第二页按 field1 升序,然后按 field2 降序”。或者,如果我需要更大的灵活性,我会使用:
http://application/products?skip=20&limit=20&sort=+field1-field2
【讨论】:
【参考方案6】:我认为版本 3 的问题更多是一个“观点”问题 - 您将页面视为页面上的资源还是产品。
如果您将页面视为资源,这是一个完美的解决方案,因为对第 2 页的查询将始终产生第 2 页。
但是,如果您将页面上的产品视为资源,则您会遇到问题,即第 2 页上的产品可能会更改(旧产品已删除或其他),在这种情况下,URI 并不总是 返回相同的资源。
例如客户存储了指向产品列表页面 X 的链接,下次打开该链接时,相关产品可能不再出现在页面 X 上。
【讨论】:
好吧,但是如果你删除了一些东西,那么同一个 URI 上就不应该有其他东西了。如果您删除页面 X 的所有产品 - 页面 X 可能仍然有效,但现在包含页面 X + 1 中的产品。因此,如果您在“产品资源视图”中看到页面 X 的 URI 已成为页面 X + 1 的 URI "。 > 如果您将页面视为资源,这是一个非常好的解决方案,因为对第 2 页的查询将始终产生第 2 页。这是否有意义?无论您是什么资源,相同的 URL(任何提到第 2 页的 URL)都将始终生成第 2 页。 将页面视为资源可能应该引入 POST /foo/page 来创建新页面,对吧? 您的答案顺利地转到“正确的解决方案是 1”,但没有说明。 在我看来,页面是一个浮动的概念,与底层域无关。因此不应被视为一种资源。我的意思是浮动的意思是它是流动的,页面的概念随着上下文的变化而变化;你的 API 的一个用户可能是一个移动应用程序,每页只能消费 2 个产品,而另一个用户是一个机器应用程序,可以消费整个该死的列表。简而言之,页面是底层域实体(产品)的“表示”,不应作为 URL 的一部分包含在内;仅作为查询参数。【参考方案7】:我目前在我的 ASP.NET MVC 应用程序中使用与此类似的方案:
例如http://application/products/by-date/page/2
具体来说是:http://application/products/Date/Ascending/3
但是,我对以这种方式在路由中包含分页和排序信息并不满意。
项目列表(在本例中为产品)是可变的。即下次有人返回包含分页和排序参数的 url 时,他们得到的结果可能已经改变。因此,http://application/products/Date/Ascending/3
作为一个指向已定义、不变的产品集的唯一 URL 的想法就消失了。
【讨论】:
第一个问题,对多列进行排序,在我看来适用于所有 3 种方法。所以这对他们中的任何一个人来说都不是真正的利弊。关于第二个问题:any 资源不会发生这种情况吗?例如,也可以编辑/删除产品。 我认为对于所有 3 种方法来说,对多列进行排序实际上是一个“骗局”,因为 url 变得更大且更难以管理 - 因此我正在考虑转向基于表单的页面/排序参数的一个原因。对于第二个问题,我认为像产品 ID 这样的唯一持久标识符与产品的临时列表之间存在根本的概念差异。对于已删除的产品,一条消息,例如“系统中不存在该产品”会告诉您有关该产品的具体信息。 从路由中删除所有的分页和排序信息是好的。并将其推入 POST 参数是不好的。你好?问题是关于 REST 的。我们不使用 POST 来缩短 REST 中的 URL。动词是有道理的。 就个人而言,我不会使用表单参数进行查询,因为它几乎需要 POST 或 PUT HTTP 方法(因为请求中现在有一个正文)。 GET 在我看来更适合使用,因为 POST 和 PUT 都意味着修改资源。因此,当需要按多列排序时,我会向 URL 添加更多查询参数。【参考方案8】:奇怪的是,没有人指出选项 3 具有特定顺序的参数。 http//application/products/Date/Descending/Name/Ascending/page/2 和 http//application/products/Name/Ascending/Date/Descending/page/2
指向同一个资源,但有完全不同的 url。
对我来说,选项 1 似乎是最容易接受的,因为它清楚地将 “我想要什么” 和 “我想要什么” 分开(它们之间甚至还有问号哈哈)。可以使用完整的 URL 实现整页缓存(无论如何,所有选项都会遇到同样的问题)。
使用 URL 中的参数方法唯一的好处是 URL 干净。尽管您必须想出一些方法来编码参数并无损解码它们。当然你可以使用 URLencode/decode,但它会让 url 再次变得丑陋 :)
【讨论】:
这是两种不同的顺序。第一个按日期降序排序,仅按名称升序打破平局;第二个按名称升序排序,仅按日期降序打破平局。 事实上,这里给出的两个示例 URL 不仅在书写上有所不同,而且在含义上也有所不同。由于表示路径,因此不能保证您在先左转和后右转时会找到相同的东西,反之亦然。话虽如此,作为 URL 路径部分的排序参数比 URL 参数具有形式上的优势,URL 参数应该在不改变整体含义的情况下可交换交换,但确实受到这里所说的编码陷阱的影响。【参考方案9】:HTTP 有很好的 Range 标头,也适合分页。您可以发送
Range: pages=1
只有第一页。这可能会迫使您重新考虑什么是页面。也许客户想要不同范围的物品。范围标头也可用于声明订单:
Range: products-by-date=2009_03_27-
获取比该日期更新的所有产品或
Range: products-by-date=0-2009_11_30
获取早于该日期的所有产品。 '0' 可能不是最好的解决方案,但 RFC 似乎想要范围开始的东西。可能部署的 HTTP 解析器无法解析 units=-range_end。
如果标题不是(可接受的)选项,我认为第一个解决方案(全部在查询字符串中)是一种处理页面的方法。但请规范化查询字符串(按字母顺序排序(键=值)对)。这解决了“?a=1&b=x”和“?b=x&a=1”的微分问题。
【讨论】:
标题乍一看可能看起来不错,但它们不允许共享页面(例如通过复制 url)。因此,对于 ajax 请求,它们可能是一个不错的解决方案(因为 ajax 修改的页面无论如何都无法在当前状态下共享),但我不会将它们用于常规分页。 并且 Range 标头仅适用于字节范围。请参阅 [HTTP 标头规范](w3.org/Protocols/rfc2616/rfc2616-sec14.html),第 14.35 节。 @ChrisWestin w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 HTTP/1.1 在 Range(第 14.35 节)和 Content-Range(第 14.16 节)标头字段中使用范围单位。range-unit = bytes-unit | other-range-unit
可能你指的是The only range unit defined by HTTP/1.1 is "bytes". HTTP/1.1 implementations MAY ignore ranges specified using other units.
那和你的说法不一样。
@Markus 我无法想象当你共享rest api资源时的用例:)
@JakubKnejzlik 共享不是问题,但使用 HTTP 标头进行分页会阻止使用 HATEOAS 链接进行分页。【参考方案10】:
选项 1 似乎是最好的,因为您的应用程序将分页视为一种为同一资源生成不同视图的技术。
话虽如此,URL 方案是相对无关紧要的。如果您将应用程序设计为 hypertext-driven(因为所有 REST 应用程序都必须按照定义),那么您的客户端将不会自行构建任何 URI。相反,您的应用程序会将链接提供给客户端,并且客户端会跟随它们。
您的客户可以提供的一种链接是分页链接。
所有这些令人愉快的副作用是,即使您改变了对分页 URI 结构的想法并在下周实施完全不同的东西,您的客户也可以继续工作而无需任何修改。
【讨论】:
很好地提醒您在 REST Web 服务中使用超媒体(如链接)。【参考方案11】:我一直使用选项 1 的样式。缓存不是问题,因为在我的情况下数据经常更改。如果你允许页面的大小是可配置的,那么数据就不能被缓存了。
我不觉得网址难记或不干净。对我来说,这是对查询参数的一种很好的使用。该资源显然是一个产品列表,查询参数只是告诉您希望列表如何显示 - 排序以及哪个页面。
【讨论】:
+1 我认为你是对的,我将使用查询参数(选项 1) “我不觉得这个 URL 很难记住”。这种观察在 REST 应用程序中是没有用的,因为这些应用程序通常应该只有一个书签......如果用户(或客户端应用程序)试图“记住”该 URL,这是 API 不平静的一个好兆头。 【参考方案12】:我倾向于同意 slf 的观点,即“页面”并不是真正的资源。另一方面,选项 3 更简洁、更易于阅读,并且用户更容易猜到甚至在必要时输入。我在选项 1 和 3 之间纠结,但看不出有任何理由不使用选项 3。
此外,虽然它们看起来不错,但正如有人提到的那样,使用隐藏参数而不是查询字符串或 URL 段的一个缺点是用户无法添加书签或直接链接到特定页面。这可能是也可能不是问题,具体取决于应用程序,但只是需要注意的事情。
【讨论】:
关于您提到更容易猜测,这应该无关紧要。如果构建超媒体 API,用户永远不必猜测 URI。【参考方案13】:我之前使用过解决方案 3(我写了很多 django 应用程序)。而且我不认为这有什么问题。它与其他两个一样可生成(以防您需要进行大规模刮擦等),并且看起来更干净。此外,您的用户可以猜测网址(如果它是面向公众的应用),而且人们喜欢能够直接前往他们想去的地方,而且猜测网址让人感觉很有力量。
【讨论】:
以上是关于REST Web 应用程序中的分页的主要内容,如果未能解决你的问题,请参考以下文章
如何在 django rest 框架的分页 URL 中更改下一个键中的主机?