📄

Goのページネーションはどうすれば良いでしょうか

2025/03/08に公開

はじめに

こんにちは、現在個人ブログをGo(Gin)とentで作りながらGoの勉強をしているKopherです。
今回はGoとentを利用してページネーション機能を実装していきたいと思います。

余談ですが、entすごく気に入ってます。

この記事で扱う技術

  • go
  • gin
  • ent

ゴール

http://localhost:8080/posts?page=1&size=10
  • ページの指定ができる
  • サイズの指定ができる
  • 降順のみ対応

実装へ

普段ならControllerのテストから作成していきますが、今回はショットカットでService, Repositoryのみの実装をお伝えることにします。

サービス層

サービスのテストから

サービス層のテストから作ります。

  • テストデータとして30個を作ります(0〜29)
  • 2ページ、10個とってきます
post_service_test.go
func TestPostController_GetAll(t *testing.T) {
	func TestPostService_GetAll_FirstPage(t *testing.T) {
	// given
	ctx := context.Background()
	postRepository := repository.NewPostRepository(client)
	postService := NewPostService(postRepository)
	t.Cleanup(func() {
		postRepository.DeleteAll(ctx)
	})

	var postCreates []*domain.PostCreate
	for i := 0; i < 30; i++ {
		postCreates = append(postCreates, &domain.PostCreate{
			Title:   fmt.Sprintf("吉祥寺マンション %d", i),
			Content: fmt.Sprintf("吉祥寺マンション購入します。 %d", i),
		})
	}
	postRepository.SaveAll(ctx, postCreates)

	// then
	search := &domain.PostSearch{
		Page: 2,
		Size: 10,
	}
	posts, _ := postService.GetAll(ctx, search)

	// then
	assert.Equal(t, "吉祥寺マンション 19", posts[0].Title)
	assert.Equal(t, "吉祥寺マンション購入します。 19", posts[0].Content)
}

PostSearchの実装

GetAllはいろんなところで呼ばれると予想されます。Offset, Limitに関する実装を修正するたびにいろんなファイルを跨いで修正することはよくないと思います。
この問題はメソッドを提供することで解決しましょう。

  • 0を指定しても1を指定しても1ページからでの扱いになります
post.go
type PostSearch struct {
	Page int
	Size int
}

func (p *PostSearch) Offset() int {
	return int(math.Max(1, float64(p.Page))-1) * p.Size
}

func (p *PostSearch) Limit() int {
	const maxSize = 100
	return int(math.Min(maxSize, float64(p.Size)))
}

サービスの本実装

サービス層ではただ、レポジトリからもらった値をレスポンスで使う構造体にマッピングしているだけですね。

post_service.go
type PostService interface {
	...
	GetAll(ctx context.Context, search *domain.PostSearch) ([]*domain.PostResponse, error)
        ...
}

...

func (p *postService) GetAll(ctx context.Context, search *domain.PostSearch) ([]*domain.PostResponse, error) {
	posts, err := p.postRepository.FindAll(ctx, search)
	if err != nil {
		return nil, err
	}
	var postResponses []*domain.PostResponse
	for _, post := range posts {
		postResponses = append(postResponses, &domain.PostResponse{
			ID:      post.ID,
			Title:   post.Title,
			Content: post.Content,
		})
	}
	return postResponses, nil
}

レポジトリ層

元java開発者だった自分としてはSpringとJPAを利用して開発を進めるとJPAのJpaRepositoryを継承してRepositoryを作ると基本的なCRUD関数は提供されるかつテストもできているのであまりRepositoryのテストを気にして無かったですが、Goではそうにはいかないですね。

  • entが提供してくれるOffset(), Limit(), Order()メソッドを使います。
  • searchnilの場合1ページを返します
post_repository.go
type PostRepository interface {
	...
	FindAll(ctx context.Context, search *domain.PostSearch) ([]*ent.Post, error)
	...
}

func (p *postRepository) FindAll(ctx context.Context, search *domain.PostSearch) ([]*ent.Post, error) {
	if search == nil {
		search = &domain.PostSearch{
			Page: 0,
			Size: 10,
		}
	}
	posts, err := p.ent.Post.Query().
		Offset(search.Offset()).
		Limit(search.Limit()).
		Order(ent.Desc(post.FieldID)).
        All(ctx)
	if err != nil {
		return nil, err
	}
	return posts, nil
}

最後に

gormではなくentがもっと広がってほしいですね。🙏

Discussion