如果您的接口返回一个包含数百或数千条记录的列表,那么出于性能或可用性的考虑,您可能需要在接口上实现分页—以便在单个HTTP响应中只返回部分数据条目。
为了说明这个特性,本节将更新GET /v1/movies接口,使它支持'pages'功能,客户端可以通过在请求中发送page和page_size参数来获取数据库中特定的movies列表,如下所示:
// 返回第一页的5条记录(1-5条数据)
/v1/movies?page=1&page_size=5
// 返回第二页的5条数据(6-10条数据
/v1/movies?page=2&page_size=5
基本上,改变page_size参数将改变每个“页面”上显示的数据数量,将page参数增加1将显示列表中的下一个“页面”的数据。
LIMIT和OFFSET子句
要实现分页的话,最简单的方式就是在数据库查询的时候,在SQL查询语句中添加LIMIT和OFFSET子句。
LIMIT子句允许您设置SQL查询能返回记录数的最大值,而OFFSET允许您在查询记录返回之前“跳过”特定的行数。
在我们的应用程序中,只需要将客户端提供的page和page_size值转换为SQL查询中的LIMIT和OFFSET适当值。数学很简单:
LIMIT = page_size
OFFSET = (page - 1) * page_size
举个具体的例子,如果客户端发送如下请求:
/v1/movies?page_size=5&page=3
我们需要将上面请求翻译成以下SQL查询:
SELECT id, created_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY %s %s, id ASC
LIMIT 5 OFFSET 10
我们可以为Filters结构体添加帮助函数来计算LIMIT和OFFSET值。更新internal/data/filters.go文件。如下所示:
package data
...
func (f Filters)limit() int {
return f.PageSize
}
func (f Filters)offset() int {
return (f.Page -1) * f.PageSize
}
在offset函数中理论上可能会发生整数溢出,因为我们将两个整数相乘。还好前面做了校验,通过ValidateFilters()函数能控制两个整数相乘不溢出。page最大值为10000000,page_size最大值100。
更新数据库模型
最后,需要更新数据模型的GetAll()方法,将LIMIT和OFFSET子句添加到SQL查询中:
File:internal/data/movies.go
func (m MovieModel)GetAll(title string, genres []string, filters Filters) ([]*Movie, error) {
// 更新SQL查询,添加LIMIT和OFFSET子句
query := fmt.Sprintf(`
SELECT id, create_at, title, year, runtime, genres, version
FROM movies
WHERE (to_tsvector('simple', title) @@ plainto_tsquery('simple', $1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY %s %s, id ASC
LIMIT $3 OFFSET $4`, filters.sortColumn(), filters.sortDirection())
//创建3s超时上下文实例
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
//因为SQL查询中有好几个占位符参数,所以我们用一个切片存放。注意我们调用limit()和offset()方法
args := []interface{}{title, pq.Array(genres), filters.limit(), filters.offset()}
rows, err := m.DB.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
//其他代码不变
}
下面可以重启服务试试效果。我们发送一个带有page_siz=2的请求:
$ curl "localhost:4000/v1/movies?page_size=2"
{
"movies": [
{
"id": 1,
"title": "Moana",
"year": 2016,
"runtime": "107 mins",
"genres": [
"animation",
"adventure"
],
"Version": 1
},
{
"id": 2,
"title": "Black Panther",
"year": 2018,
"runtime": "134 mins",
"genres": [
"sci-fi",
"action",
"adventure"
],
"Version": 2
}
]
}
看起来不错。接口返回数据库中的前两条movies记录。(使用默认排序方式,按照movie的ID升序排列)
我们再发送一个获取下一页movies的请求。如果你跟随本书操作的话,你将看到接口返回数据库中后续的记录:
$ curl "localhost:4000/v1/movies?page_size=2&page=2"
{
"movies": [
{
"id": 4,
"title": "The Breakfast Club",
"year": 1985,
"runtime": "97 mins",
"genres": [
"comedy",
"drama"
],
"Version": 15
}
]
}
如果你尝试请求第三页数据,将得到一个空的JSON数组响应结果:
$ curl "localhost:4000/v1/movies?page_size=2&page=3"
{
"movies": null
}
网友评论