REST Web应用程序中的分页


329

这是对该问题的更一般化的表述(省去了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
我个人认为使用此方法没有问题,但是有人警告我这不是一个好方法(他没有给出原因,因此,如果您知道为什么不建议这样做,请告诉我)

任何建议,意见,批评都将受到欢迎。谢谢。


34
这是一个很好的问题。
伊恩·霍尔德

7
额外的问题:人们通常如何指定页面大小?
Heiko Rupp 2013年

Answers:


66

我认为版本3的问题更多是“观点”问题-您将页面视为资源还是页面上的产品。

如果您将页面视为资源,那是一个很好的解决方案,因为对页面2的查询将始终产生页面2。

但是,如果您将页面上的产品视为资源,则可能会遇到页面2上的产品可能会更改(删除了旧产品等)的问题,在这种情况下,URI并不总是返回相同的资源。

例如,客户存储了指向产品列表页面X的链接,下次打开链接时,所涉及的产品可能不再位于页面X上。


6
好吧,但是如果您删除某些内容,则同一URI上不应有其他内容。如果删除页面X的所有产品,页面X可能仍然有效,但现在包含页面X + 1中的产品。因此,如果您在“产品资源视图”中看到页面X的URI,则页面X + 1的URI已成为页面X + 1的URI。 ”。
Fionn

1
>如果您将页面视为资源,则它是一个很好的解决方案,因为对页面2的查询将始终产生页面2。这是否有意义?不管您是什么资源,相同的URL(任何提及页面2的URL)将始终产生页面2。
temoto

2
将页面视为资源可能应该引入POST / foo / page来创建新页面,对吗?
temoto

18
您的答案顺利地转到“正确的解决方案是1”,但没有说明。
temoto

2
在我看来,页面是一个浮动的概念,与基础领域无关。因此,不应将其视为资源。我的意思是说浮动是流动的,页面的概念随上下文而改变;您的API的一个用户可能是一个移动应用程序,每页只能使用2种产品,而另一个是可以消耗整个列表的机器应用程序。简而言之,页面是基础域实体(产品)的“表示形式”,不应包含在URL中;仅作为查询参数。
Kingz 2014年

106

我同意Fionn的观点,但是我要进一步说,页面对我来说不是资源,而是请求的属性。这使我只选择了选项1查询字符串。感觉不错。我非常喜欢Twitter API的结构结构。不太简单,也不太复杂,有据可查。不管是好是坏,当我以一种方式与另一种方式做事时,这是我的“去”设计。


28
+1:查询字符串不是一流的资源标识符;他们只是澄清了资源的排序和分组。
S.Lott

1
@ S.Lott请求资源。Fielding在其论文的5.2.1.1节中将所谓的“一流资源”定义为。此外,在同一部分中,Fielding给出了源代码文件的最新修订,作为资源示例。那怎么可能是资源,而最新的10个产品是“产品资源请求的属性”?我了解您的观点较为实用,但我认为它的REST风格较差。
edsioufi 2013年

请注意,我的评论并不意味着我不同意在URL上使用查询字符串的选择:只要API是超媒体驱动的,两者都是可行的解决方案,就像@RichApodaca在他的回答中提到的那样。我只是指出,从REST的角度来看,应该将Page视为一种资源。
edsioufi 2013年

37

HTTP具有出色的Range标头,也适用于分页。您可以发送

Range: pages=1

只有第一页。这可能会迫使您重新考虑什么是页面。也许客户想要不同范围的物品。范围标头还可以用于声明订单:

Range: products-by-date=2009_03_27-

获得比该日期更新的所有产品,或者

Range: products-by-date=0-2009_11_30

使所有产品都早于该日期。“ 0”可能不是最佳解决方案,但是RFC似乎需要一些用于范围开始的内容。可能部署了HTTP解析器,无法解析unit = -range_end。

如果标题不是(可接受的)选项,我认为第一个解决方案(全部在查询字符串中)是一种处理页面的方法。但是,请规范化查询字符串(按字母顺序对(键=值)对进行排序)。这解决了“Δa= 1&b = x”和“Δb= x&a = 1”的区分问题。


34
标题乍一看可能看起来不错,但它们不允许共享页面(例如,通过复制url)。因此,对于ajax请求,它们可能是一个不错的解决方案(因为无论如何都无法在其当前状态下共享由ajax修改的页面),但是我不会将其用于常规分页。
Markus

3
并且Range标头仅用于字节范围。请参阅[HTTP标头规范](w3.org/Protocols/rfc2616/rfc2616-sec14.html),第14.35节。
克里斯·威斯汀,

16
@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.那与您的陈述不同。
temoto 2012年

1
@Markus我无法想象共享共享api资源时的用例:)
JakubKnejzlik 2015年

@JakubKnejzlik共享不是问题,但是使用HTTP标头进行分页会阻止使用HATEOAS链接进行分页。
xarx

25

在您的应用程序将分页视为一种用于产生同一资源的不同视图的技术的情况下,选项1似乎是最好的。

话虽如此,URL方案相对来说并不重要。如果您将应用程序设计为超文本驱动的(因为所有REST应用程序都必须是按定义的),那么您的客户端将不会自己构造任何URI。相反,您的应用程序将提供到客户端的链接,客户端将跟随它们。

客户可以提供的一种链接是分页链接。

所有这一切的令人愉快的副作用是,即使您改变了对分页URI结构的看法并在下周实施了完全不同的操作,您的客户也可以继续工作而无需进行任何修改。


3
很好地提醒您在REST Web服务中使用超媒体之类的链接。
Paul D. Eden

11

我一直使用选项1的样式。由于我的情况下数据经常更改,因此缓存并不是一个问题。如果您允许页面的大小是可配置的,那么将无法再次缓存数据。

我发现该网址难以记住或不干净。对我来说,这是查询参数的一种很好的用法。该资源显然是一个产品列表,查询参数只是在告诉您希望列表如何显示-排序以及哪个页面。


1
+1我认为您是对的,我将使用查询参数(选项1)
andi

“我觉得该网址不容易记住”。这种观察在REST应用程序中是没有用的,因为它们通常只应具有一个书签...如果用户(或客户端应用程序)试图“记住” URL,则表明该API并不宁静。
edsioufi 2013年

8

奇怪的是没有人指出选项3具有特定顺序的参数。 http // application / products / Date / Descending / Name / Ascending / page / 2http // application / products / Name / Ascending / Date / Descending / page / 2

指向相同的资源,但具有完全不同的网址。

对我来说,选项1似乎是最可接受的,因为它清楚地将“我想要的”“我想要的”分开(它们之间甚至有问号,大声笑)。可以使用完整URL来实现全页缓存(无论如何,所有选项都会遇到相同的问题)。

使用“ URL中的参数”方法,唯一的好处就是干净的URL。尽管您必须想出一些方法来编码参数并无损地解码它们。当然,您可以使用URLencode / decode,但这会使URL再次变得丑陋:)


1
这是两个不同的顺序。第一个按日期降序排列,仅按名字升序断开关系;第二个按名称升序排序,仅按日期降序打断关系。
Imran Rashid 2015年

实际上,此处给出的两个示例URL不仅在书写上有所不同,而且在含义上也有所不同。由于表示路径,因此不能保证先左后右都可以找到相同的东西,反之亦然。话虽如此,排序参数作为URL路径的一部分比URL参数具有形式上的优势,URL参数应该可以互换地交换而不改变总体含义,但确实存在编码陷阱的问题,如此处所述。
Christian Gosch 2015年

7

我更喜欢使用查询参数offset和limit。

offset:用于集合中项目的索引。

限制:用于项目计数。

客户端可以简单地继续更新偏移量,如下所示

offset = offset + limit

用于下一页。

该路径被视为资源标识符。页面不是资源而是资源集合的子集。由于分页通常是GET请求,因此查询参数最适合分页而不是标头。

参考:https : //metamug.com/article/rest-api-developers-dilemma.html#Requesting-the-next-page


5

在寻找最佳实践时,我遇到了这个网站:

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中,还有一些针对更具体情况的建议。


4

我目前在ASP.NET MVC应用程序中使用类似于此的方案:

例如 http://application/products/by-date/page/2

具体来说是: http://application/products/Date/Ascending/3

但是,我对以这种方式在页面中包括分页和排序信息并不十分满意。

项目列表(在这种情况下为产品)是可变的。也就是说,下次有人返回包含分页和排序参数的网址时,他们得到的结果可能已更改。因此,http://application/products/Date/Ascending/3作为指向定义的,不变的产品集的唯一url 的想法丢失了。


1
我认为,第一个问题是对多列进行排序,适用于所有3种方法。因此,对于任何一个人来说,这都不是真正的利弊。关于第二个问题:这不会发生在任何资源上吗?例如,产品也可以被编辑/删除。
andi

我认为对于所有3种方法,在多列上进行排序实际上是一个“骗局”,因为url变得越来越大且更难以管理-因此,我正在考虑转向基于表单的页面/排序参数的原因之一。对于第二个问题,我认为像产品ID这样的唯一持久标识符与产品的暂态列表之间存在根本的概念差异。对于已删除的产品,系统会显示一条消息,例如“该产品在系统中不存在”。
史蒂夫·威尔考克

1
从路由中删除所有分页和排序信息是很好的。并将其推送到POST参数中是不好的。你好?问题是关于REST。我们不使用POST只是为了使REST中的URL更短。动词是有道理的。
temoto

1
就个人而言,我不会在查询中使用表单参数,因为它几乎需要POST或PUT HTTP方法(因为请求中现在有一个正文)。GET在我看来更喜欢使用更合适的方法,因为POST和PUT都暗示着修改资源。因此,当需要按多列进行排序时,我会向URL添加更多查询参数。
Paul D. Eden

1

我倾向于同意“页面”并不是真正的资源。另一方面,选项3更干净,更易于阅读,并且用户更容易猜出,甚至在必要时也可以键入。我在选项1和3之间感到困惑,但是没有任何理由不使用选项3。

同样,尽管它们看起来不错,但正如有人提到的那样,使用隐藏参数而不是查询字符串或URL段的一个缺点是用户无法添加书签或直接链接到特定页面。取决于应用程序,这可能是问题,也可能不是问题,只是一些需要注意的问题。


1
关于您提到的更容易猜测的问题,这无关紧要。如果构建超媒体API,则用户永远不应猜测URI。
JR加西亚

0

我之前使用过解决方案3(我写了很多django应用)。而且我认为这没有任何问题。它与其他两个一样可生成(以防您需要进行大量刮擦等操作),并且看起来更干净。另外,您的用户可以猜测url(如果它是面向公众的应用程序),并且人们喜欢能够直接转到他们想要的地方,并且url猜测可以增强功能。


0

我在项目中使用以下网址:

http://application/products?page=2&sort=+field1-field2

这意味着-“给我页面第二页,按字段1升序排列,然后按字段2降序排列”。或者,如果我需要更大的灵活性,可以使用:

http://application/products?skip=20&limit=20&sort=+field1-field2

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.