📃

Goでページネーション機能を実装

2024/09/27に公開

はじめに

Goでページネーション機能を実装します。
今回レスポンスで返すデータ一覧は下記の通りです。

  • "data"
     対象データ
  • "page"
     現在のページ
  • "limit"
    取得件数の範囲
  • "totalItems"
     データの総件数
  • "totalPages"
     ページの総件数

ライブラリ

  • GORM (Postgres driver): v1.5.9
     ORMライブラリで、PostgreSQLデータベースに対するオブジェクト操作を簡素化する。

  • Gin: v1.10.0
     Go言語の高性能なWebフレームワークで、APIやWebアプリケーションの開発に利用する。

  • Godotenv: v1.5.1
     .envファイルから環境変数をロードし、開発環境での設定管理を容易にする。

  • CompileDaemon: v1.4.0
     ファイル変更を監視し、自動でGoアプリケーションのビルドと再起動を行う開発支援ツール。

実装

main.go
package main

import (
	"fmt"

	"example.com/go-pagination/controllers"
	"example.com/go-pagination/initializers"
	"github.com/gin-gonic/gin"
)

func init() {
	initializers.LoadEnvVariables()
	initializers.ConnectToDB()
	initializers.SyncDatabase()
}

func main() {
	r := gin.Default()

	r.GET("/pagination", controllers.GetPaginatedUsers)
	r.Run()
	fmt.Println("Hello, World!")
}

ページネーションのロジック。ページと取得件数の範囲をクエリで取得する。

paginationController.go
package controllers

import (
	"net/http"
	"strconv"

	"example.com/go-pagination/initializers"
	"example.com/go-pagination/models"
	"github.com/gin-gonic/gin"
)

func GetPaginatedUsers(c *gin.Context) {
	// デフォルトのページとリミットの設定
	pageStr := c.Query("page")
	limitStr := c.Query("limit")

	page, err := strconv.Atoi(pageStr)
	if err != nil || page < 1 {
		page = 1
	}

	limit, err := strconv.Atoi(limitStr)
	if err != nil || limit < 1 {
		limit = 10 // デフォルトで1ページあたり10件
	}

	var users []models.User
	var totalUsers int64

	offset := (page - 1) * limit

	// ページネーションされたデータを取得しつつ、総件数をカウント
	result := initializers.DB.Model(&models.User{}).
       Order(models.User{}.ID).
		Limit(limit).
		Offset(offset).
		Find(&users).
		Offset(-1). // Reset offset for counting
		Count(&totalUsers)

	if result.Error != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "データの取得に失敗しました"})
		return
	}

	// 総ページ数を計算
	totalPages := (totalUsers + int64(limit) - 1) / int64(limit)

	// ページネーション結果のレスポンスを返す
	c.JSON(http.StatusOK, gin.H{
		"data":       users,
		"page":       page,
		"limit":      limit,
		"totalItems": totalUsers,
		"totalPages": totalPages,
	})
}

検証

今回、DBに32件のテストデータを登録しておいた。
今回は10件ずつのデータで2ページ目のデータを取得する。
下記の通り、想定通りの対象データが取得できていることがわかる。

おまけ・パフォーマンス改善について

今回のコードだと書き方はシンプルになるがDBには2回アクセスすることになる(下記のSQLのログの通り)

パフォーマンスの観点からアクセスは1回にしたい。その場合は下記のような実装になる。
しかし、これだとSQLをハードコーディングすることになるため、この書き方もあまり良くない。(カラム名の変更時に自動変換できない、コンパイル時に気付けない)
もし、この記事を見た有識者で何か良い実装方法があればコメントで教えていただきたい。

	result := initializers.DB.Model(&models.User{}).
	Select("*, (SELECT COUNT(*) FROM users) as total_users").
	Limit(limit).
	Offset(offset).
	Scan(&users)

Discussion