My Note

自己理解のためのブログ

GoのWebフレームワーク Echoでの開発記録 (GoのORM( GORM )でPaginationを利用する)

はじめに

個人開発中のWebアプリでPaginationのページを作成する機会があったので調べながらやってみました。 開発環境は Go1.6 + Echo + MySQLコンテナ + Redisコンテナで、ORマッパーに "gorm" を利用し、htmlのページは Goでレンダリングしています。

■ GORMのドキュメント

ページングの利用方法について説明されています。 gorm.io

コード

ディレクトリ構造

├── README.md
├── api
│   ├── crypto
│   │   ├── bat
│   │   │   └── bat.go
│   │   ├── coincheck
│   │   │   └── coincheck.go
│   │   └── gmo
│   │       └── gmo.go
│   ├── healthcheck
│   │   ├── healthcheck.go
│   │   └── healthcheck_test.go
│   ├── metal
│   │   └── metal.go
│   └── stock
│       └── investment_trust.go
├── app.yaml
├── conf
│   └── config.go
├── docker
│   ├── mysql
│   │   ├── Dockerfile
│   │   ├── data
│   │   └── my.cnf
│   └── redis
│       └── data
├── docker-compose.yml
├── go.mod
├── go.sum
├── handler
│   ├── auth
│   │   └── auth.go
│   ├── crypto
│   │   └── crypto.go
│   ├── graph
│   │   └── crypto.go
│   ├── hash
│   │   └── hash.go
│   └── top.go
├── main.go
├── model
│   ├── base.go
│   ├── crypto
│   │   ├── daily_rate
│   │   │   └── daily_rate.go
│   │   ├── exchange
│   │   │   └── exchange.go
│   │   ├── exchange_result
│   │   │   └── result.go
│   │   ├── token
│   │   │   └── token.go
│   │   ├── token_result
│   │   │   └── result.go
│   │   └── trade_history
│   │       └── trade_history.go
│   ├── date
│   │   └── date.go
│   ├── stock
│   │   └── investment_trust
│   │       └── investment_trust.go
│   └── user
│       └── user.go
├── public
│   └── assets
│       └── css
│           ├── crypto_register.css
│           ├── crypto_summary.css
│           ├── crypto_trade_history.css
│           ├── login.css
│           ├── mypage.css
│           └── signup.css
├── script
│   └── setup_local_env.sh
├── secret.yaml
├── staticcheck.conf
└── view
    ├── crypto_register.html
    ├── crypto_summary.html
    ├── crypto_trade_history.html
    ├── login.html
    ├── mypage.html
    ├── signup.html
    └── top.html

■ 利用するDBのテーブルは以下。

mysql> SELECT id,user_id, token_id, exchange_id,trade,date,volume,rate,price FROM crypto_trade_histories LIMIT 1;
+----+---------+----------+-------------+-------+-------------------------+-----------+---------+-------+
| id | user_id | token_id | exchange_id | trade | date                    | volume    | rate    | price |
+----+---------+----------+-------------+-------+-------------------------+-----------+---------+-------+
|  1 |       1 |        1 |           3 | buy   | 2021-07-03 00:00:00.000 | 0.0026152 | 3823799 | 10000 |
+----+---------+----------+-------------+-------+-------------------------+-----------+---------+-------+

mysql> SELECT id, token FROM tokens;
+----+-------+
| id | token |
+----+-------+
|  1 | BTC   |
|  2 | ETH   |
+----+-------+

mysql> SELECT id, exchange FROM exchanges;
+----+-----------+
| id | exchange  |
+----+-----------+
|  1 | gmo       |
|  2 | rakuten   |
|  3 | coincheck |
+----+-----------+

model/crypto/trade_history/trade_history.go(一部抜粋)

  • 構造体配列に結果を渡して、Paginate() にて LIMIT句とOFFSET句を利用して結果を分割してページネーションで表示させる。
  • GetTradeHistories() はページ番号とそのページの検索結果を返す
type TradeHistory struct {
    Exchange string
    Token    string
    Trade    string
    Volume   float64
    Rate     int
    Price    int
    Date     string
}

func GetTradeHistories(c echo.Context, userID int) ([]TradeHistory, int) {
    var tradeHistory []TradeHistory
    //db.Raw("SELECT exchange, token, trade, rate, price, DATE_FORMAT(date, '%Y-%m-%d') AS date FROM crypto_trade_histories INNER JOIN exchanges on crypto_trade_histories.exchange_id = exchanges.id INNER JOIN tokens on crypto_trade_histories.token_id = tokens.id  ORDER BY date desc").
    db.Scopes(Paginate(c.Request())).
        Table("crypto_trade_histories").
        Select("exchange, token, trade, volume, rate, price, DATE_FORMAT(date, '%Y-%m-%d') AS date").
        Joins("inner join exchanges on crypto_trade_histories.exchange_id = exchanges.id").
        Joins("inner join tokens on crypto_trade_histories.token_id = tokens.id").
        Where("user_id = ?", userID).
        Order("date desc").
        Scan(&tradeHistory)
    q := c.Request().URL.Query()
    page, _ := strconv.Atoi(q.Get("page"))

    return tradeHistory, page
}

func Paginate(r *http.Request) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        q := r.URL.Query()
        page, _ := strconv.Atoi(q.Get("page"))
        if page == 0 {
            page = 1
        }
        pageSize, _ := strconv.Atoi(q.Get("page_size"))
        switch {
        case pageSize > 100:
            pageSize = 100
        case pageSize <= 0:
            pageSize = 10
        }

        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}
  • htmlにレンダリングするために、マップ(key にstring、value に構造体)を用意して値を展開する
  • マップにするデータは、"取引履歴" と "ページURL"
    • TopPageは1ページ目に移動
    • BackPageは1ページ戻る
    • NextPageは次のページに進む

handler/crypto.go (一部抜粋)

type Page struct {
    TopPage  string
    BackPage string
    NextPage string
}

func ShowTradeHistoryPage(c echo.Context) error {
    session := auth.GetSession(c)
    if session.Values["auth"] != true {
        return c.Redirect(301, "/login")
    }
    username := session.Values["username"].(string)
    userID := user.GetUserID(username)

    tradeHistory, page := trade_history.GetTradeHistories(c, userID)
    p := make([]Page, 1, 1)
    p[0].TopPage = "/crypto/trade_history?page=" + strconv.Itoa(1)
    p[0].BackPage = "/crypto/trade_history?page=" + strconv.Itoa(page-1)
    p[0].NextPage = "/crypto/trade_history?page=" + strconv.Itoa(page+1)

    data := map[string]interface{}{
        "trade_history": &tradeHistory,
        "page":          &p,
    }
    return c.Render(http.StatusOK, "crypto_trade_history", data)
}
  • main.go(一部抜粋 Staticファイルの設定)
   // Staticファイル
    e.Static("/assets", "public/assets")
    e.Static("/crypto/assets", "public/assets")
    e.GET("/crypto/trade_history", crypto.ShowTradeHistoryPage)
  • func ShowTradeHistoryPage(c echo.Context) error {} で定義した dataの key名( "trade_history" と "page" ) を指定して、それをrangeで回して valueに 構造体のフィールド名を指定する

view/crypto_trade_history.html (cssに関してはコピペ利用できるサンプルを参照して作成。本記事ではコードは割愛)

{{define "crypto_trade_history"}}
<!DOCTYPE html>
<html lang="jp">
<head>
    <link rel="stylesheet" href="assets/css/crypto_trade_history.css">
</head>
<header>
    <p class="logo">Crypto</a><span>取引履歴</span></p>
</header>
<body>
    <table class="osare5-table col5t">
        <thead>
            <tr>
                <th>取引所</th>
                <th>トークン</th>
                <th>ボリューム</th>
                <th>売買</th>
                <th>レート</th>
                <th>金額</th>
                <th>日時</th>
            </tr>
        </thead>
        <tbody>
            {{range .trade_history}}
            <tr>
                <td>{{.Exchange}}</td>
                <td>{{.Token}}</td>
                <td>{{.Volume}}</td>
                <td>{{.Trade}}</td>
                <td>{{.Rate}}</td>
                <td>{{.Price}}</td>
                <td>{{.Date}}</td>
            </tr>
            {{end}}
        </tbody>
    </table>
    <p></p>
    {{range .page}}
    <a href="{{.TopPage}}" class="button">top</a>
    <a href="{{.BackPage}}" class="button">back</a>
    <a href="{{.NextPage}}" class="button">next</a>
    {{end}}
    <p></p>
    <a href="/mypage" class="button">MyPageへ</a>
</body>
</html>
{{end}}

結果

ページURLのクエリパラメータがページ番号になりページネーションで表示させ、遷移する事ができました。