美文网首页
【Go Web开发】返回分页元数据

【Go Web开发】返回分页元数据

作者: Go语言由浅入深 | 来源:发表于2022-02-27 14:16 被阅读0次

    上一篇文章,GET /v1/movies接口的分页功能已经实现了,但如果我们能在响应中包含一些额外的元数据,那就更好了。像当前页码和最后页码这样的信息,以及数据记录的总数,将有助于向客户端提供关于响应上下文,并使页面导航更容易。

    本节我们将改进响应内容,使其包含额外的分页元数据,类似如下:

    {
        "metadata":
        {
            "current_page": 1,
            "page_size": 20,
            "first_page": 1,
            "last_page": 42,
            "total_records": 832
        },
        "movies":
        [
            {
                "id": 1,
                "title": "Moana",
                "year": 2015,
                "runtime": "107 mins",
                "genres":
                [
                    "animation",
                    "adventure"
                ],
                "version": 1
            }
            ...
        ]
    }
    

    计算数据总数

    这部分实现最困难的地方在于计算total_records值。我们需要根据过滤器,过滤之后的总数。而不是数据库中movies表中的所有电影记录总数。

    要做到这一点,一个简单的方法是调整我们现有的SQL查询,包括一个window函数,它计算过滤后的总行数,像这样:

    SELECT count(*) OVER(), 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 $3 OFFSET $4
    

    上面的count(*) OVER()表达式会计算过滤后的数据总数,并以第一个值返回。

    count id created_at title year runtime genres version
    3 1 2020-11-27 17:17:25+01 Moana 2015 107 {animation,adventure} 1
    3 2 2020-11-27 18:01:45+01 Black Panther 2018 134 {sci-fi,action,adventure} 2
    3 4 2020-11-27 18:02:20+01 The Breakfast Club 1985 97 {comedy,drama} 6

    当PostgreSQL执行这个SQL查询时,操作顺序大致如下所示:

    1、WHERE子句用于过滤movies表中的数据并获得符合条件的行。

    2、window函数count(*) OVER()被执行,计算所有过滤后的行数。

    3、执行ORDER BY规则,对查询出来的行进行排序。

    4、执行LIMIT和OFFSET规则,返回符合要求的部分,即分页功能。

    更新代码

    有了上面的解释,我们开始修改代码。首先更新internal/data/filters.go文件,定义Metadata结构体存储分页元数据,并添加帮助函数来计算值。如下所示:

    File:internal/data/filters.go


    package data
    
    ...
    
    type Metadata struct {
        CurrentPage  int `json:"current_page,omitempty"`
        PageSize     int `json:"page_size,omitempty"`
        FirstPage    int `json:"first_page,omitempty"`
        LastPage     int `json:"last_page,omitempty"`
        TotalRecords int `json:"total_records,omitempty"`
    }
    
    
    // 计算Metadata各字段值,例如,如果查询总的行数为12, pageSize等于5,最后一页等于math.Ceil(12/5)=3
    func calculateMetadata(totalRecords, page, pageSize int) Metadata {
        if totalRecords == 0 {
            return Metadata{}
        }
        return Metadata{
            CurrentPage:  page,
            PageSize:     pageSize,
            FirstPage:    1,
            LastPage:     int(math.Ceil(float64(totalRecords) / float64(pageSize))),
            TotalRecords: totalRecords,
        }
    }
    

    然后,我们需要更新GetAll()方法,以使用新的SQL查询(使用window函数)来获得总行数。然后,如果一切正常,我们将使用calculateMetadata()函数来生成分页元数据,并将其与查询数据一起返回。

    File: internal/data/movies.go


    package data
    
    ...
    
    func (m MovieModel)GetAll(title string, genres []string, filters Filters) ([]*Movie, Metadata, error)  {
        // 使用window函数,计算查询总数
        query := fmt.Sprintf(`
            SELECT count(*) OVER(), 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, Metadata{}, err
        }
        defer rows.Close()
    
        totalRecords := 0
        var movies []*Movie
        for rows.Next(){
            var movie Movie
            err := rows.Scan(
                &totalRecords,
                &movie.ID,
                &movie.CreateAt,
                &movie.Title,
                &movie.Year,
                &movie.Runtime,
                pq.Array(&movie.Genres),
                &movie.Version,
                )
            if err != nil {
                return nil, Metadata{}, err
            }
            movies = append(movies, &movie)
        }
        if err = rows.Err(); err != nil {
            return nil, Metadata{}, err
        }
        // 根据查询数据生成Metadata实例
        metadata := calculateMetadata(totalRecords, filters.Page, filters.PageSize)
        return movies, metadata, nil
    }
    

    最后,还需要更新接口处理程序listMoviesHandler来接收GetAll()方法返回的Metadata结构体实例,并将其添加到JSON返回给客户端。如下所示:

    File: cmd/api/movies.go


    package main
    
    ...
    
    func (app *application) listMoviesHandler(w http.ResponseWriter, r *http.Request) {
        var input struct {
            Title  string
            Genres []string
            data.Filters
        }
    
        v := validator.New()
        qs := r.URL.Query()
    
        input.Title = app.readString(qs, "title", "")
        input.Genres = app.readCSV(qs, "genres", []string{})
    
        input.Filters.Page = app.readInt(qs, "page", 1, v)
        input.Filters.PageSize = app.readInt(qs, "page_size", 20, v)
    
        input.Filters.Sort = app.readString(qs, "sort", "id")
        input.Filters.SortSafelist = []string{"id", "title", "year", "runtime", "-id", "-title", "-year", "-runtime"}
        //检查校验是否通过
        if data.ValidateFilters(v, input.Filters); !v.Valid(){
            app.failedValidationResponse(w, r, v.Errors)
            return
        }
    
        //调用 GetAll()方法从数据库中查询movie记录
        movies, metadata, err := app.models.Movies.GetAll(input.Title, input.Genres, input.Filters)
        if err != nil {
            app.serverErrorResponse(w, r, err)
            return
        }
        err = app.writeJSON(w, http.StatusOK, envelope{"movies": movies, "metadata": metadata}, nil)
        if err != nil {
            app.serverErrorResponse(w, r, err)
        }
    }
    

    重启服务,然后测试GET /v1/movies接口。你会发现响应中包含分页的元数据信息:

    $ curl "localhost:4000/v1/movies?page=1&page_size=2"
    {
            "metadata": {
                    "current_page": 1,
                    "page_size": 2,
                    "first_page": 1,
                    "last_page": 2,
                    "total_records": 3
            },
            "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
                    }
            ]
    }
    

    并且如果你尝试使用过滤器进行请求,你会看到last_page值和total_records的变化反映了应用查询参数过滤效果。例如,通过只请求带有“adventure”类型的电影,我们可以看到total_records计数下降到2:

    $ curl "localhost:4000/v1/movies?genres=adventure"  
    {
            "metadata": {
                    "current_page": 1,
                    "page_size": 20,
                    "first_page": 1,
                    "last_page": 1,
                    "total_records": 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
                    }
            ]
    }
    

    最后,如果你用一个很大的page值请求,会得到一个空的元数据对象和movies数组的响应,像这样:

    $  curl "localhost:4000/v1/movies?page=100"
    {
            "metadata": {},
            "movies": null
    }
    

    在过去的几节中,我们在GET /v1/movies接口上做了大量工作。但最终的结果很好的。客户端现在可以对其响应包含的内容进行很多控制,支持过滤、分页和排序。

    通过创建Filters结构体,可以轻松地将一些公共的东西放到需要分页和排序功能的接口中。如果回头看一下我们在listMovieHandler和数据库模型的GetAll()方法中编写的代码,会发现这里的代码并不比之前的接口代码多多少。

    相关文章

      网友评论

          本文标题:【Go Web开发】返回分页元数据

          本文链接:https://www.haomeiwen.com/subject/ypjhrrtx.html