本节我们将开始使用查询字符串参数,以便用户可以搜索具有特定标题或类型的电影。
具体来说,我们将构建一个简化的过滤器,允许客户端根据电影标题不区分大小写和一个或多个电影类型的精确匹配进行搜索。例如:
// 查询所有movies
/v1/movies
// 查询title内容和'balck panther'匹配的电影
/v1/movies?title=black+panther
// 查询title为moana,电影类型genres包含“animation“和“adventure”。
/v1/movies?title=moana&genres=animation,adventure
上面查询参数中的“+“号,在URL编码中指的是空格符。你也可以使用%20代替。两种方式在URL中都表示空格。
SQL动态过滤
构建动态过滤最困难的部分是创建查询数据库的SQL语句—我们需要在标题和类型上都使用过滤器,或者只在其中一个上使用过滤器。
为了解决这个问题,一种选择是在运行时动态构建SQL查询……为连接或插入到WHERE子句中的每个过滤器提供必要的过滤条件。但是这种方法会使您的代码变得混乱和难以理解,特别是对于需要支持大量过滤器选项的大型查询。
本书中我们选择另一种方法,使用固定的SQL查询:
SELECT id, create_at, title, year, runtime, genres, version
FROM movies
WHERE (LOWER(title) = LOWER($1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY id
这个SQL查询的设计使得每个过滤器的行为都像是“可选的”。例如,条件(LOWER(title) = LOWER($1) OR $1 = '') 占位符$1和数据库中title在不区分大小写情况下匹配或者为空就返回记录。因此,当搜索的电影标题是空字符串“”时,这个过滤条件实际上将被“跳过”。
同样(genres @> $2 OR $2 = '{}')筛选条件,@>是包含的意思。条件返回true,如果占位符$2中的值都包含在数据库genres字段,或者占位符为空也返回true。
你应该记得,在本系列文章的前面,我们设置了listMoviesHandler,使用空字符串“”和空切片作为标题和类型过滤器参数的默认值:
input.Title = app.readString(qs, "title", "")
input.Genres = app.readCSV(qs, "genres", []string{})
把这些结合在一起,意味着,如果用户没有提供title查询字符串参数,$1占位符的值是空字符串""。SQL查询的过滤条件将返回true,类似跳过过滤操作。genres参数也是如此。
PostgreSQL对数组字段提供了很多操作符,包括&&重叠操作,@<操作符以及array_length()函数。完整列表可以查看这里
下面我们回到internal/data/movies.go文件,更新GetAll()方法,使用这个查询语句:
File:internal/data/movies.go
package data
...
func (m MovieModel)GetAll(title string, genres []string, filters Filters) ([]*Movie, error) {
query := `
SELECT id, create_at, title, year, runtime, genres, version
FROM movies
WHERE (LOWER(title) = LOWER($1) OR $1 = '')
AND (genres @> $2 OR $2 = '{}')
ORDER BY id`
//创建3s超时上下文实例
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
//使用QueryContext()来执行查询
rows, err := m.DB.QueryContext(ctx, query, title, pq.Array(genres))
if err != nil {
return nil, err
}
defer rows.Close()
var movies []*Movie
for rows.Next(){
var movie Movie
err := rows.Scan(
&movie.ID,
&movie.CreateAt,
&movie.Title,
&movie.Year,
&movie.Runtime,
pq.Array(&movie.Genres),
&movie.Version,
)
if err != nil {
return nil, err
}
movies = append(movies, &movie)
}
if err = rows.Err(); err != nil {
return nil, err
}
return movies, nil
}
现在让我们重新启动应用程序,并使用之前给出的示例进行测试。如果你一直跟随操作,如下所示:
$ curl "localhost:4000/v1/movies?title=black+panther"
{
"movies": [
{
"id": 2,
"title": "Black Panther",
"year": 2018,
"runtime": "134 mins",
"genres": [
"sci-fi",
"action",
"adventure"
],
"Version": 2
}
]
}
$ curl "localhost:4000/v1/movies?genres=adventure"
{
"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
}
]
}
$ curl "localhost:4000/v1/movies?title=moana&genres=animation,adventure"
{
"movies": [
{
"id": 1,
"title": "Moana",
"year": 2016,
"runtime": "107 mins",
"genres": [
"animation",
"adventure"
],
"Version": 1
}
]
}
您还可以发送不匹配任何记录的过滤器请求。在这种情况下,你应该在响应中得到一个空的JSON数组,如下所示:
$ curl "localhost:4000/v1/movies?genres=western"
{
"movies": null
}
进展很顺利。我们的API接口现在返回经过适当过滤的电影记录,并且我们有一个模式,可以很容易地扩展该模式以便在未来使用其他过滤规则(例如关于电影year或runtime过滤器)。
网友评论