はじめに
この記事では、Gormで行うPaginetionについて述べたいと思います。
また、paginationと同時にソートや検索等を行うことが多くあると思い、こちらに関しても記述させていただきました。
サンプルコードとして、Ginを使って行っていますが、echoでやる場合もほぼ同様にできます。
参考
- Gormの公式pagination
https://gorm.io/docs/scopes.html#Pagination - 特に参考にした記事
https://dev.to/rafaelgfirmino/pagination-using-gorm-scopes-3k5f
github
githubにコードをまとめたので、分からない箇所等があれば見てみてください。
フォルダ名がpagenationになっているのは見てみぬふりをしてくれると助かります🙏
全体のAPI構成
今回、以下のような構成で行っています。
controllerやserviseに関しては今回の記事とは趣旨が異なるので説明は割愛させていただきます。
構成に関して以下の記事を参考にさせていただいています。
https://qiita.com/Asuforce/items/0bde8cabb30ac094fcb4
.
├── controller
│ └── shop_controller.go #apiのエンドポイント
├── db
│ └── db.go # dbのInitやClose
├── dto
│ ├── page.go #pageのmodel
│ └── shop_dto.go #レスポンスデータ(dto)
├── entity
│ └── shop.go #entityオブジェクト
├── main.go
├── mapper
│ └── page_mapper.go #pageobjへの変換
├── query # gormのquery生成
│ ├── pagination.go
│ └── sort.go
├── server
│ └── server.go # routing等を記載
└── service
└── shop_service.go # entityとdtoの変換
レスポンス
今回、レスポンスとしては以下のようなものを想定しています。
自分はpaginationの情報をpageでラッピングしていますが、_metadataのようにすると汎用性が高まるかもしれません。
https://stackoverflow.com/questions/12168624/pagination-response-payload-from-a-restful-api
RequestUrl: http://localhost:8080/shops?size=2&&page=1&&direction=asc&&orderby=name
{
"page": {
"number": 2, # page number
"size": 2, #contents size
"total_elements": 7, # total size
"total_pages": 4 # total pages
},
"shops": [
{
"id": 7,
"name": "Beauty-Beauty",
"created_at": "2021-11-21T16:42:43.655756Z"
},
{
"id": 3,
"name": "Ceauty-Beauty",
"created_at": "2021-11-21T16:42:43.651779Z"
}
]
}
扱うエンティティ
今回扱うEntityです。shopsテーブルを例として扱います。
package entity
import "time"
type Shop struct {
Id uint `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
モデルの作成
まず、pageのモデルを生成していきます。
package dto
type Page struct {
Number int `json:"number"`
Size int `json:"size"`
TotalElements int `json:"total_elements"`
TotalPages int `json:"total_pages"`
}
pageの構造体には、現在の表示ページ(number)、1ページあたりに表示する件数(size)、件数(total_elements)、最後に全体のページ数(total_pages)を定義しています。
これだけの情報があれば、view上でpaginationを表現する際に必要な情報としては十分かと思います。
引用元: https://terasolunaorg.github.io/guideline/5.0.0.RELEASE/ja/ArchitectureInDetail/Pagination.html
また、レスポンスとして返信するshopのdtoクラスも作成します。
レスポンスとしてはdbから受け取ったshopの配列及び、pageの情報を返します。
`
type ShopDto struct {
Page Page json:"page"
Shops []entity.Shop json:"shops"
}
### mapperの作成
次に、mapperを生成します。
mapperではurlのqueryパラメーターと、データの大きさ(totalElements)を元にpageのオブジェクトを生成しています。
```go
package mapper
import (
"log"
"math"
"strconv"
"github.com/DaisukeHirabayashi/golang-pagenation/dto"
"github.com/gin-gonic/gin"
)
func ConvertContextAndTotalElementsToPage(context *gin.Context, totalElements int) dto.Page {
page, _ := strconv.Atoi(context.Query("page"))
if page == 0 {
page = 1
}
pageSize, _ := strconv.Atoi(context.Query("size"))
switch {
case pageSize > totalElements:
pageSize = totalElements
case pageSize > 100:
pageSize = 100
case pageSize <= 0:
if totalElements < 5 {
pageSize = totalElements
} else {
pageSize = 5
}
}
totalPages := int(math.Ceil(float64(totalElements) / float64(pageSize)))
return dto.Page{Number: page, Size: pageSize, TotalElements: totalElements, TotalPages: totalPages}
}
例として、下記のようなurlでリクエストを送信した場合、1ページあたりに表示する件数を2件、表示するpageを1としてpageオブジェクトを生成しています。
http://localhost:8080/shops?size=2&&page=1
また、queryパラメーターとしてsizeやpage情報を送信しなかった場合には初期値としてsize=5,page=1としています。
queryの作成
ここでは、pagenationの情報を基に、dbから必要な情報のみを取得するようにQueryを生成します。
package query
import (
"github.com/DaisukeHirabayashi/golang-pagenation/dto"
"github.com/jinzhu/gorm"
)
type Pagination struct{}
func (pagination Pagination) Pagination(page dto.Page) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
offset := (page.Number - 1) * page.Size
return db.Offset(offset).Limit(page.Size)
}
}
pageオブジェクトの内容を元にオフセットや個数を定義しQueryとして生成しています。
serviceの作成
package service
import (
"github.com/DaisukeHirabayashi/golang-pagenation/db"
"github.com/DaisukeHirabayashi/golang-pagenation/dto"
"github.com/DaisukeHirabayashi/golang-pagenation/entity"
"github.com/DaisukeHirabayashi/golang-pagenation/mapper"
"github.com/DaisukeHirabayashi/golang-pagenation/query"
"github.com/gin-gonic/gin"
)
type ShopService struct{}
var query_pagination query.Pagination
func (shopService ShopService) GetShops(context *gin.Context) (dto.ShopDto, error) {
db := db.GetDB()
var shops []entity.Shop
totalElements := db.Find(&shops).RowsAffected
var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements))
if err := db.Scopes(query_pagination.Pagination(page)).Find(&shops).Error; err != nil {
return dto.ShopDto{}, err
}
return dto.ShopDto{Page: page, Shops: shops}, nil
}
ここでは、ページネーションの情報に必要となる、データの総数を以下のようにして取得します。
totalElements := db.Find(&shops).RowsAffected
その後、mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements))
にて、totalElementsとurlのqueryパラメータを元にpageオブジェクトに変換します。
また、ScopeやFindによるQueryによってdbからデータを取得しています。
最後に、レスポンスとして必要となる情報をdto.ShopDto{Page: page, Shops: shops}
にて返しています。
その他
検索
where句等で検索したものに対してpaginationを行うには、以下のようにすることで対処可能です。
今回のコードでは、全体の個数が特定されればpaginationができるようになっています。
したがって、検索で今回のコードを使いたい場合、serviceの箇所を以下のように変更することで可能となります。
totalElements := db.Where("title LIKE ? ","%"+context.Query("keyword")+"%")Find(&shops).RowsAffected
var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements))
if err := db.Scopes(query_pagination.Pagination(page)).Where("title LIKE ? ","%"+context.Query("keyword")+"%").Find(&shops).Error; err != nil {
return dto.ShopDto{}, err
}
}
ソート
ソートを行いたい場合、ソートのための関数をまず作成します。
package query
import (
"strings"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
)
type Sort struct{}
func (sort Sort) Sort(context *gin.Context) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
direction := context.Query("direction")
if direction != "asc" && direction != "desc" {
direction = "desc"
}
orderby := context.Query("orderby")
if orderby == "" {
orderby = "id"
}
order := strings.Join([]string{orderby, direction}, " ")
return db.Order(order)
}
}
ここでは、ソートの初期値をidの降順としています。
その後、検索の際と同様に以下のようにすることで対応可能です。
ソートの場合には全体の個数は変わらないのでtotalElementsの記述は変わりません。
totalElements := db.Find(&shops).RowsAffected
var page dto.Page = mapper.ConvertContextAndTotalElementsToPage(context, int(totalElements))
if err := db.Scopes(query_pagination.Pagination(page)).Scopes(query_sort.Sort(context)).Find(&shops).Error; err != nil {
return dto.ShopDto{}, err
}
まとめ
今回、gormでのpaginationについてまとめてみました。
どのコードでも扱えるように共通化したつもりなのでよかったら参考にしてみてください。
以下、行ったことのまとめです。
- データの総数をもとにpageのオブジェクト生成
- pageのオブジェクトをもとに、dbからデータを取り出す。
- pagenationを扱いたい場合、
Scopes(query_pagination.Pagination(page))
をメソッドチェーンでつなげる。