sqlserver分页查询关键字_创建分页的正确姿势

创建分页的正确姿势

下面这些话,是在我职业生涯的几个项目中,经常喃喃的自言自语抱怨的事。

不过,与我偶尔不得不发问“为什么这个API没有分页”的不满相比,这件事上的不满是什么呢?让我从无服务器架构的混乱工作中抽出一点空来谈谈分页。

单向分页和双向分页


通俗的讲,我见过两种常见类型的分页:

  • 简单粗暴的单向分页,将结果集一次性返回,冗长而低效——例如:推特的粉丝列表,或是谷歌的搜索结果;
  • 双向分页则通过一系列参数反馈或以流的形式实现,在收到第一页的结果后追加显示新的结果——例如:推特的推文时间线,或者通知系统;

避免抽象泄漏(Leaky abstraction)


我经常见到的一个错误的做法是,要求调用者提供一个"key"来用于结果集的排序,例如按时间戳(timestamp)或者按字母顺序来排序,这样就产生了一个“抽象泄漏”——调用者必需了解服务器端的底层机制才能使用分页。

DynamoDB数据库的查询API就是这样一个例子。为了将查询的结果集分页,调用者必须在一系列的请求中提供ExclusiveStartKey,服务器也要在响应中包含LastEvaluatedKey。

在实际使用过程中,你几乎可以把LastEvaluatedKey作为一个token或是cursor来对待,简单的把它放到下一页或者上一页请求中来传递。但是它不仅是一个token,它的属性名暴露了服务端的实现细节,它实际上是一个在DynamoDB数据库进行排序用的key。

就其本身而言,这没什么大不了的。但是,不幸的是,它经常会产生成一些连锁反应,那就是这样鼓励应用程序的开发人员在进行应用程序开发时,将底层的实现细节暴露在分页信息中;下图这种情况中,客户端就需要负责跟踪相应的信息了。

e29759935c15982eba4374573a521927.png

恭喜你,你的数据库用来支持分页的底层机制现在已经全部泄漏给前端了!

使分页的意图明确且一致


另一种我常见的现象是,你必须一次又一次的把相同的请求参数发送给分页的API,例如:

  • 每页结果的最大数量
  • 分页的方向是上一页还是下一页(双向分页系统)
  • 原始查询语句(在DynamoDB数据库中,包括一系统的属性,像过滤表达式,关键字条件表达式,映射表达式和索引名)

当偶尔来这样做时,我不会把它当作一个错误来看待,但是这样缺乏设计的机制让我很烦恼。在我遇到的所有分页API中,预期的行为始终是获取查询的下一组结果,而不是中途可以做其他查询。这样做并不合理,实际上都不把这样的机制叫做分页,更应该称其为导航!合理的做法应该是从之前接收的页面更改分页的方向。当我们在本文中进一步讨论双向分页时,将对此进行更详尽的介绍。

单向带cursor的分页


对于单向分页,我首选的做法是使用简单的cursor。这里重要的一个点是要使cursor变的无意义。

对于客户端而言,当有更多的结果要提取时,服务器应该在响应中返回一个Blob。客户端不应该从中获取任何实现细节,唯一可以做的是在下一个请求中通过cursor将其发送出去。

f420c538749e86060eb3f57fb6540348.png

API怎样知道从哪里开始获取下一页数据


一个简单的做法是这样的:

  • 创建一个JSON对象去捕获需要获取的下一页数据——例如:如果你用的是DynamoDB,那么就是请求下一页对象(包含ExclusiveStartKey)
  • 用base64将JSON字符串编码
  • 将base64 blob作为cursor返回

当我们收到下一页请求时,我们可以简单的用相反的流程从请求中获取我们创建的JSON对象。

如果其他人用base64去解码blob呢

这是肯定的,所以你可以选择先把JSON进行加密。你也可以选择不把DynamoDB请求作为分页基础。

你是否注意到了,在图1和图2的例子中,客户端仅在后续请求中发送cursor?这是设计使然。

我之前提到过,客户端已经告诉我们请求是第一次请求,分页机制只在后续的请求中提供分页信息。对于我来说,这意味着分页机制不应该包含任何其他行为,除了上一次请求的cursor。反过来说,这意味着我们需要在curor中捕获取原始查询条件,以便我们构造数据库查询语句。或者我们在cursor中获取实际数据库请求,这似乎又是一个简单而实用的解决方案。

2031c48607885b6a7e57437c4517d639.png

双向分页中使用cursor

使用双向分页时,你需要能够及时向前(将新的内容添加到时间线中)和向后(获取旧的内容)进行分页。因此简单的字符串cursor已经不能满足需求,我们需要两个cursor,每个方向使用一个例如:

{    "before": "iwnVn1a8N",    "after": "M82B111C"}

此外,在向前分页时,即使没有更多的结果,我们仍需要返回cursor,因为以后可以将新结果集添加到源中。因为我们还需要一对布尔标识:

{    "before": "iwnVn1a8N",    "hasBefore": true,    "after": "M82B111C",    "hasAfter": true}

当客户端获取分页信息并接收到 hasAfter标识为 false时,它就知道后续暂时没有更多结果。因此它可以决定不去主动获取下一页的结果,而变为被动定期轮询新结果。

让我们用一个简单的例子来说明一下,想象一下如果你推特的时间线中获取推文,那么API会首先返回最新的推文。

ae1039470dc98404323ca3169006851c.png
  • 客户端开始发出第一个请求
  • API返回一个带cursor的结果对象,因为是最后一页结果集, hasAfter标识为 false,但是 hasBefore标识为 true,因为较早的内容是有的
  • 客户端将前一个cursor加入到第二个请求中,然后发出请求,这使得其目标清晰明确
  • API返回另一个携带 hasBefore和 hasAfter都为 `true的curor对象,因为我们正好在结果流的中间
  • 客户端发出第三个也就是最后一个请求,再次仅传递从前一个响应接收到的before cursor
  • API返回一个 hasBefore为 false的cursor对象,应该我们已经没有更旧的结果了

好的,让我们再运行另一个例子,这次我们按时间线向前翻页。

fdd2d02a87c0119c6d71b8d5ad918f92.png
  • 客户端发出了第一个请求
  • API返回一个带cursor的结果对象, hasAfter为 false因为API返回的是最新结果集,而 hasBefore为 true因为较旧的结果集还有
  • 过了一段时间,有更多的新结果了
  • 客户端发出了第二个请求,仅将'after cursor'在加入到了请求中,让目标清晰明确
  • API仅以客户端未收到的新结果集作为响应,而响应中的cursor.hasAfter为false,因为这次返回的为最新结果;如果客户端很快又向后翻页,那么它会收到与API第一个响应相同一结果

现在,让我们回到之前提过的分页过程中偶尔需要改变方向的问题。

我们分页的原因是因为将所有的结果集一次性返回是不切实际而且低效的,例如:把凯蒂·佩里的108M的推特粉丝列表在一次请求-响应的过程中返回,那么服务器和app都会瘫掉。

除了限制在一次请求-响应过程中返回数据的多少之外,我们还需要对app将缓存的数据量设置上限,以提升用户体验防止app崩溃。

e9aada4fca060ffe2f7b68ae4fb35a5f.png

这意味着,在某个时间点,随着用户不断滚动浏览旧推文,客户端将需要开始删除已获取的数据,否则可以会将内存耗尽。也就是说,当用记向后举动查看推文时,客户端将需要重新获取已删除的数据,从而反转分页的原始方向。

10c76a3a05cb39146712ff4bb396df21.png

幸运的是上面所述的方案足够灵活,可以满足你做到这一点。每一页结果都有一个关联的cursor,使你可以从任何方向获取到下一页。因此如果你需要获取被销毁的页面数据,就像你获取最新页面一样简单。

f73fd2eb2b14ea6b44c97cf075a0b353.png

处理间隔

还是拿推特的例子来说。如果关闭推特app后,过段时间再打开推特,你发现推文已经被缓存到了app中;你也就意识到所有的从缓存中进行分页的方法不够灵活。相反,客户端可以使用非分页请求获取最新的推文。向下滚动时客户端按照图中所示的方法用自动获取较旧的页面,并填补间隔,直到它与缓存的数据结合在一起。

3afd63543a3094818954c6030f395194.png

推特app的策略随着时间的推移发生了改变,我看到的另一种策略是在时间轴中放一个可视可点击的标记,以标记缺失的推文。这使得用户有了可以选择的空间,来自己决定是否翻阅旧的推文来填补间隔。

这就是我分享的实现单向和双向分页的简单有效的方法,希望它对你有用!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值