LoginSignup
16
6

More than 1 year has passed since last update.

[Golang]Gormで行うPagination(sort対応)

Last updated at Posted at 2021-11-23

はじめに

この記事では、Gormで行うPaginetionについて述べたいと思います。
また、paginationと同時にソートや検索等を行うことが多くあると思い、こちらに関しても記述させていただきました。
サンプルコードとして、Ginを使って行っていますが、echoでやる場合もほぼ同様にできます。

参考

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を表現する際に必要な情報としては十分かと思います。

Screen Shot 2021-11-22 at 17.55.33.png
引用元: 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))をメソッドチェーンでつなげる。
16
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
6