REST API 设计:过滤、排序和分页
API 设计正在成为 API 产品战略的核心支柱,无论 API 是公共的还是私有的。一个好的 API 设计可以改善应用程序的整体开发体验,并可以提高性能和长期的可维护性。
但是,没有标准或官方的 API 设计指南,RESTful 只是一种架构风格。
过滤
URL 参数是向 REST API
添加基本过滤的最简单方法。如果有一个 /items
端点是待售商品,可以通过属性名称进行过滤,例如 GET /items?state=active
或 GET /items?state=active&seller_id=1234
。通常这仅适用于精确匹配,如果想执行价格或日期范围等范围过滤该怎么办呢?
问题是 URL 参数只有一个键和一个值,但过滤器由三个组件组成:
属性或字段名称
eq
、lte
、gte
等运算符过滤器值
有多种方法可以将三个组件编码为 URL 参数键/值。
LHS Brackets
对运算符进行编码的一种方法是在键名上使用方括号 []
。例如,GET /items?price[gte]=10&price[lte]=100
意思是查找价格大于或等于 10 但小于或等于 100 的所有项目。
可以根据需要使用任意数量的运算符,例如 [lte]
、[gte]
、[exists]
、[regex]
、[before]
和 [after]
。
LHS Brackets 在服务器端解析有点困难,但为客户端的过滤器值提供了更大的灵活性,无需以不同方式处理特殊字符。
好处
方便客户端使用。有许多查询字符串解析库可以轻松地将嵌套的 JSON 对象编码到方括号中。
qs
就是这样一个自动编码/解码方括号的库:
易于在服务器端解析。URL 参数键包含字段名称和运算符。
GROUP BY
无需查看 URL 参数值即可轻松(属性名称、运算符)。当操作符被用作文字过滤术语时,不需要转义过滤值中的特殊字符。当过滤器包含用户可能设置的额外自定义元数据字段时,尤其如此。
缺点
可能需要在服务器端做更多工作来解析和分组过滤器。可能需要编写自定义 URL 参数绑定器或解析器,以将查询字符串键拆分为两个组件:字段名称和运算符。然后需要 GROUP BY(财产名称,运营商)。
变量名中的特殊字符可能很尴尬。可能需要编写一个自定义绑定器来将查询字符串键拆分为两个组件:字段名称和运算符。
难以管理自定义组合过滤器。具有相同属性名和操作符的多个过滤器将导致隐式 and 。如果 API 用户想要或过滤器怎么办。例如,找到所有价格小于 10 或大于 100 的项目?
RHS Colon
与 LHS Brackets 方法类似,可以设计一个 API 来在 RHS 而不是 LHS 上使用运算符。例如,GET /items?price=gte:10&price=lte:100
将查找价格大于或等于 10 但小于或等于 100 的所有项目。
好处
在服务器端最容易解析,尤其是在不支持重复过滤器的情况下。不需要自定义粘合剂,许多 API 框架已经处理 URL 参数数组。多个价格过滤器将位于同一变量下,该变量可能是序列或 Map。
缺点
文字值需要特殊处理。例如,GET /items?user_id=gt:100
将转换为查找 user_id
大于 100 的所有项目。但是,如果想查找 user_id
等于 gt:100
的所有项目,因为这可能是有效 ID,该怎么办?
搜索查询参数
如果需要在端点上进行搜索,可以直接使用搜索参数添加对过滤器和范围的支持。如果已经在使用 ElasticSearch
或其他基于 Lucene
的技术,可以直接支持 Lucene
语法或 ElasticSearch
简单查询字符串。
例如,可以搜索名称包含 red chair
并且价格大于或等于 10 且小于或等于 100 的项目:GET /items?q=title:red chair AND price:[10 TO 100]
此类 API 可以允许模糊匹配、增强某些术语等。
好处
API 用户最灵活的查询
后端几乎不需要解析,可以直接传递给搜索引擎或数据库(为了安全,请注意清理输入)
缺点
初学者更难开始使用 API。需要熟悉
Lucene
语法。全文搜索对所有资源都没有意义。例如,模糊度和术语提升对时间序列指标数据没有意义。
需要 URL 百分比编码,这使得使用
cURL
或Postman
更加复杂。
分页
大多数返回实体列表的端点都需要支持某种分页。如果没有分页,简单的搜索可能会返回数百万甚至数十亿次点击,从而导致无关的网络流量。
分页需要隐含的排序。默认情况下,这可能是项目的唯一标识符,但也可以是其他有序字段,例如创建日期。
偏移分页
这是最简单的分页形式。 limit/offset
在使用 SQL 数据库的应用程序中变得流行,这些 SQL 数据库已经将 LIMIT
和 OFFSET
作为 SQL SELECT
语法的一部分。实现 limit/offset
分页只需要很少的业务逻辑。
limit/offset
分页看起来像 GET /items?limit=20&offset=100
,此查询将返回从第 100 行开始的 20 行。
好处
最容易实现,除了直接将参数传递给 SQL 查询之外几乎不需要编码。
在服务器上无状态。
无论自定义
sort_by
参数如何都有效。
缺点
对于较大的偏移值,性能不佳。假设执行一个偏移值为
1000000
的查询。数据库需要扫描和计数从 0 开始的行,并将跳过(即丢弃数据)前1000000
行。将新项目插入表格时不一致(即页面漂移) 当按最新先订购项目时,这一点尤其明显。考虑以下通过减少 Id 进行排序:
询问
GET /items?offset=0&limit=15
表中添加了 10 个新项目
查询
GET /items?offset=15&limit=15
第二个查询将只返回 5 个新项目,因为添加 10 个新项目会将偏移量移回 10 个项目。为了解决这个问题,客户端确实需要为第二个查询偏移 25 ,即变成GET /items?offset=25&limit=15
,但是客户端不可能知道其他对象被插入到表中。
即使有限制,偏移分页也很容易实现和理解,并且可以用于数据集具有较小上限的应用程序。
键集分页
键集分页使用最后一页的过滤器值来获取下一组项目,这些列将被索引。
好处
与现有过滤器配合使用,无需额外的后端逻辑。只需要一个额外的限制 URL 参数。
即使将较新的项目插入表中,也能保持一致的顺序,在按最近排序时效果很好。
即使有较大的偏移量也能保持一致的性能。
缺点
分页机制与过滤器和排序的紧密耦合。即使没有过滤器,也强制 API 用户添加过滤器。
不适用于低基数字段,例如枚举字符串。
使用自定义
sort_by
字段时 API 用户很复杂,因为客户端需要根据用于排序的字段调整过滤器。
键集分页可以很好地用于具有单个自然高基数键的数据,例如可以使用时间戳的时间序列或日志数据。
寻找分页
Seek Paging 是 Keyset 寻呼的扩展。通过添加 after_id
或 start_id
的 URL 参数,可以消除分页与过滤器和排序的紧密耦合。由于唯一标识符自然是高基数,因此不会遇到与按状态枚举或类别名称等低基数字段排序不同的问题。
基于搜索的分页的问题是在需要自定义排序顺序时很难实现。
实例(假设查询按创建日期升序排列)
客户请求最近的项目:
GET /items?limit=20
在滚动/下一页上,客户端从先前返回的结果中找到“20”的最后一个 id。然后使用它作为起始 id 进行第二次查询:
GET /items?limit=20&after_id=20
在滚动/下一页上,客户端从先前返回的结果中找到“40”的最后一个 id。然后使用它作为起始 id 进行第三次查询:
GET /items?limit=20&after_id=40
排序
与过滤一样,排序对于任何返回大量数据的 API 端点都是一个重要的特性。如果返回一个用户列表,API 用户可能希望按最后修改日期或电子邮件排序。
为了启用排序,大部分 API 都使用了一个sort
或 sort_by
的 URL 参数,该参数接受排序字段名作为值。
但是,好的 API 设计可以灵活地指定升序或降序。与过滤器一样,指定顺序需要将三个组件编码到一个键/值对中,如下:
多列排序
要对这种多列排序进行编码,可以允许多个字段名称,例如:
总结
一个好的 API 设计可以改善应用程序的整体开发体验,并可以提高性能和长期的可维护性。但是随着项目迭代的进行,API 设计通常会受到业务逻辑复杂度的影响。
版权声明: 本文为 InfoQ 作者【devpoint】的原创文章。
原文链接:【http://xie.infoq.cn/article/13612b5a04a98058d57bbd89d】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论