0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goのページネーション関数

Posted at

初めに

現在、クリーンアーキテクチャ設計を元にして開発している案件においてページネーション関数を実装する機会があったので自分用のメモとして記事を書きました。
拙い文章もあるかとは存じますがご了承ください。

1.ページネーション関数

interactor配下に下記のファイルを追加。

pagination_interactor.go
package interactor

import (
	"math"

	"github.com/example/pkg/domain/model"
	"github.com/example/pkg/interfaces/gateway/database"
)

func Paginate(q database.Queryable, page, limit int) (database.Queryable, *model.PageInfo, error) {
	var (
		totalCount int64
		err        error
	)

	// ページ番号とリミットが 0 以下の場合はデフォルト値を設定
	if page <= 0 {
		page = 1
	}
	if limit <= 0 {
		limit = 10
	}

	if totalCount, err = q.Count(); err != nil {
		return nil, nil, err
	}

	pageInfo := &model.PageInfo{
		TotalCount:  int(totalCount),
		CurrentPage: page,
	}

	// 最後のページ番号を計算
	pageInfo.LastPage = int(math.Ceil(float64(totalCount) / float64(limit)))

	// 次のページが存在するかどうかを判定
	pageInfo.HasNext = page < pageInfo.LastPage

	// オフセットを計算
	offset := (page - 1) * limit

	// クエリに Limit と Offset を設定
	q = q.Limit(limit).Offset(offset)

	return q, pageInfo, nil
}

2.実装ファイルの例

下記の例はconfigファイルやwireの実装、及び説明を省いております。
案件ではwireを使って依存関係を管理したり、DB接続の情報などはconfigファイルに記述すべきですがそれだと説明が非常に長くなってしまった為、流れが把握できる最小限のコード絞っております。

apiのセットアップファイル

api.go
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/example/pkg/interfaces/api"
)

type API struct {
	Engine *gin.Engine
	Port   string
}

func NewAPI(
	genresAPI api.GenresController,
) *API {
	router := &API{
		Engine: gin.New(),
		Port:   config.Port,
		Config: config,
	}
	genres := router.Engine.Group("/genres")
	{
		genres.GET("/", func(c *gin.Context) { genresAPI.Search(c) })
	}
	return router
}

func (r *API) Run() error {
	return r.Engine.Run(r.Port)
}

Controller層

genres_controller.go
package api

import (
	"net/http"

	"github.com/example/pkg/usecase/input"
	"github.com/example/pkg/usecase/interactor"
)

type GenresController struct {
	ApplicationController
	genreSearchInteractor interactor.GenreSearchInteractor
}

func (c GenresController) Search(ctx *Context) {
	context := c.CreateContext(ctx)

	genres, err := c.genreSearchInteractor.Invoke(context)
	if err != nil {
		c.Abort(ctx, err)

		return
	}

	ctx.JSON(http.StatusOK, genres)
}


func NewGenresController(
	base ApplicationController,
	genreSearchInteractor interactor.GenreSearchInteractor,
) GenresController {
	return GenresController{
		ApplicationController: base,
		genreSearchInteractor: genreSearchInteractor,
	}
}

Interactor層
本来はvalidateチェックなどをすべきですが、今回は設計とページネーション関数について説明したいので省きます。

genre_interactor.go
package interactor

import (
	"context"

	"github.com/example/pkg/domain/model"
	"github.com/example/pkg/usecase/output"
	"github.com/example/pkg/usecase/repository"
)

type GenreSearchInteractor struct {
	genreRepository repository.GenreRepository
	presenter       output.GenreSearchPresenter
}

func (i GenreSearchInteractor) Invoke(ctx context.Context) (*output.Genres, error) {
	genres, err := i.genreRepository.Search(ctx)
	if err != nil {
		return i.presenter.Error(err)
	}

	return i.presenter.Output(genres)
}

func NewGenreSearchInteractor(
	genreRepository repository.GenreRepository,
	presenter output.GenreSearchPresenter,
) GenreSearchInteractor {
	return GenreSearchInteractor{
		genreRepository: genreRepository,
		presenter:       presenter,
	}
}

usecase/repositoryのインターフェース

genre_repository.go(interfaces)
package repository

import (
	"context"

	"github.com/example/pkg/domain/model"
)

type GenreRepository interface {
	Search(ctx context.Context) ([]*model.Genre, error)
}

interfaces/gatewayにあるrepositoryの実装ファイル
ORMはGORMを使用しております。
詳しく知りたい方は公式サイトをご覧ください。

genre_repository.go(impl)
package repository

import (
	"context"

	"github.com/example/pkg/domain/model"
	"github.com/example/pkg/interfaces/gateway/database"
	repo "github.com/example/pkg/usecase/repository"
)

type GenreRepository struct {
	db database.Client
}

func (r *GenreRepository) Search(ctx context.Context) ([]*model.Genre, error) {
	var (
		err    error
		conn   database.Connection
		genres []*model.Genre
	)

	if conn, err = r.db.Reader(ctx); err != nil {
		return nil, err
	}

	q := conn.Query().From("genres").OrderBy("genre_code")

	if err = q.Find(&genres); err != nil {
		return nil, err
	}

	return genres, nil
}

func NewGenreRepository(db database.Client) repo.GenreRepository {
	return &GenreRepository{
		db: db,
	}
}

Entities層

genre.go
package model

type Genre struct {
	ID        uint64    `json:"id" gorm:"primary_key;"`
	GenreCode      string  `json:"GenreCode" gorm:"not null; size:20"`
	GenreName      string  `json:"GenreName" gorm:"not null; size:50"`
	GenreRomanName *string `json:"GenreRomanName" gorm:"size:50"`
    CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}
domain/page_info.go
package model

type PageInfo struct {
	Count       int  `json:"count"`
	TotalCount  int  `json:"totalCount"`
	HasNext     bool `json:"hasNext"`
	CurrentPage int  `json:"currentPage"`
	LastPage    int  `json:"lastPage"`
}

Input層

genre.go
package input

type GenreCreateInput struct {
	GenreCode      string  `json:"genreCode" validate:"required,max=20"`
	GenreName      string  `json:"genreName" validate:"required,max=50"`
	GenreRomanName *string `json:"genreRomanName" validate:"omitempty,max=50"`
}

Output層

genre.go
package output

type PageInfo struct {
	PageInfo PageInfoDTO `json:"pageInfo"`
}

type PageInfoDTO struct {
	Count       int  `json:"count"`
	TotalCount  int  `json:"totalCount"`
	HasNext     bool `json:"hasNext"`
	CurrentPage int  `json:"currentPage"`
	LastPage    int  `json:"lastPage"`
}

Presenter層

genre_presenter.go
package presenter
import (
	"github.com/example/pkg/domain/model"
	"github.com/example/pkg/usecase/output"
)

type genreSearch struct{}

func (d genreSearch) Output(genres []*model.Genre, pageInfo *model.PageInfo) (*output.Genres, error) {
	var outputGenres []output.GenreDTO
	for _, genre := range genres {
		outputGenres = append(outputGenres, TransformGenreDTO(genre))
	}

	outputPageIngo := TransformPageInfoDTO(pageInfo)

	return &output.Genres{
		PageInfo: outputPageIngo,
		Genres:   outputGenres,
	}, nil
}

func (d genreSearch) Error(err error) (*output.Genres, error) { return nil, err }

func NewGenreSearchPresenter() output.GenreSearchPresenter {
	return genreSearch{}
}

func TransformGenreDTO(genre *model.Genre) output.GenreDTO {
	return output.GenreDTO{
		ID:             genre.ID,
		GenreCode:      genre.GenreCode,
		GenreName:      genre.GenreName,
		GenreRomanName: genre.GenreRomanName,
	}
}

実際の使用例

contorollerでpageとlimitをリクエストから受け取ります。
そしてそれをinteractorに渡してあげます。

genres_controller.go
import (
	"net/http"
	"strconv"

	"github.com/example/pkg/usecase/input"
	"github.com/example/pkg/usecase/interactor"
)
type GenresController struct {
	ApplicationController
	genreSearchInteractor interactor.GenreSearchInteractor
}
func (c GenresController) Search(ctx *Context) {
	context := c.CreateContext(ctx)

	page, _ := strconv.Atoi(ctx.DefaultQuery("page", "1"))
	limit, _ := strconv.Atoi(ctx.DefaultQuery("limit", "10"))

	genres, err := c.genreSearchInteractor.Invoke(context, page, limit)
	if err != nil {
		c.Abort(ctx, err)

		return
	}
	ctx.JSON(http.StatusOK, genres)
}
genre_interactor.go
func (i GenreSearchInteractor) Invoke(ctx context.Context, page int, limit int) (*output.Genres, error) {
	genres, pageInfo, err := i.genreRepository.Search(ctx, page, limit)
	if err != nil {
		return i.presenter.Error(err)
	}

	return i.presenter.Output(genres, pageInfo)
}
genre_repository.go
// 省略
type GenreRepository struct {
	db database.Client
    interactor interactor.pagination_interactor
}

func (r *GenreRepository) Search(ctx context.Context, page int, limit int) ([]*model.Genre, *model.PageInfo, error) {
    var (
		err      error
		conn     database.Connection
		genres   []*model.Genre
		pageInfo *model.PageInfo
	)

    if conn, err = r.db.Reader(ctx); err != nil {
       return nil, nil, err
    }

    q, pageInfo, err = interactor.Paginate(q, page, limit)
    if err != nil {
       return nil, nil, err
    }
    
    q = q.Query().From("genres").OrderBy("genre_code")
	if err = q.Find(&genres); err != nil {
		return nil, err
	}

    pageInfo.Count = len(genres)

    return genres, pageInfo, nil
}

func NewGenreRepository(
    db database.Client,
    interactor interactor.pagination_interactor
) repo.GenreRepository {
	return &GenreRepository{
		db: db,
        interactor: interactor
	}
}

ページを1、リミットを5に設定することでDBに保存されてある15のデータのうち、0~4個目のデータを取得することができました。

request

curl --location 'http://localhost/genres?page=1&limit=5' 

response

{
    "pageInfo": {
        "pageInfo": {
            "count": 5,
            "currentPage": 1,
            "hasNext": true,
            "lastPage": 3,
            "totalCount": 15
        }
    },
    "genres": [
        {
            "genreCode": "1",
            "genreName": "ジャンル1",
            "genreRomanName": "genre 1",
            "id": 1
        },
        {
            "genreCode": "2",
            "genreName": "ジャンル2",
            "genreRomanName": "genre 2",
            "id": 2
        },
        {
            "genreCode": "3",
            "genreName": "ジャンル3",
            "genreRomanName": "",
            "id": 3
        },
        {
            "genreCode": "4",
            "genreName": "ジャンル4",
            "genreRomanName": "",
            "id": 4
        },
        {
            "genreCode": "5",
            "genreName": "ジャンル5,
            "genreRomanName": "",
            "id": 5
        }
    ]
}
0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?