📄
Goのページネーションはどうすれば良いでしょうか
はじめに
こんにちは、現在個人ブログを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()
メソッドを使います。 -
search
がnil
の場合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