Help us understand the problem. What is going on with this article?

GoのRDBアクセスライブラリ(go-pg/pg)の紹介

はじめに

GoのRDBアクセスライブラリ、皆さんは何を利用していますでしょうか。
今回見つけたgo-pg/pgが、自分的にめちゃくちゃイケてたので紹介します。

go-pg/pgの日本語記事の第一号です!!(自分調べ)

go-pg/pgとは

名前からもお察しの通り、PostgreSQLに特化したDBアクセスライブラリです。
次項に記載しますが、私が調べた中でもっとも機能が充実していました。

Document: https://pg.uptrace.dev/
Pkg: https://pkg.go.dev/github.com/go-pg/pg/v10
Git: https://github.com/go-pg/pg

機能

その前に

以下のような機能をもつDBライブラリってあるのかなと調査したのが、そもそもの発端でした。

  1. SELECTはRawSQLで書きたい(ORMは読みにくい)
  2. RawSQLでは変数名でバインドしたい
  3. Insert, Update, DeleteはORMでやりたい(SQLだと冗長)
  4. Insert, Update, DeleteはBulkクエリをしたい(1クエリで複数レコード処理)

go-pg/pgの対抗馬としてgormも多機能で候補に上がりましたが、
上記の条件を全て満たすのはgo-pg/pgだけでした。

以降は代表的な機能とちょっと詰まりそうな注意点を紹介します。
興味が出て、網羅的に機能を確認したい方はドキュメントをご確認ください。

Bulk処理

  • go-pg/pgはBulkInsert, BulkUpdate, BulkDelete、全て可能です。
  • 例えばBulkUpdateのクエリは以下のようになります。PKでレコードを一意に特定した上での1クエリになっています。
  • ちなみに、gormはBulkInsertのみ対応してます。他のライブラリも私の知る限りBulkUpdateが対応しているものはないです。
UPDATE "table_objs" AS "table_obj"
SET "str1" = _data."str1", "time1" = _data."time1", "num1" = _data."num1", "created_at" = _data."created_at", "updated_at" = _data."updated_at"
FROM (VALUES
 ('r1k1'::text, 'r1k2'::text, 'testStr1'::text, NULL::timestamptz, '8.88'::numeric(4,2), '2021-01-01 16:36:29.869198+00:00:00'::timestamptz, '2021-01-01 16:36:31.877413+00:00:00'::timestamptz),
 ('r2k1'::text, 'r2k2'::text, 'testStr2'::text, NULL::timestamptz, '0.12'::numeric(4,2), '2021-01-01 16:36:29.869198+00:00:00'::timestamptz, '2021-01-01 16:36:31.877414+00:00:00'::timestamptz)
) AS _data("key1", "key2", "str1", "time1", "num1", "created_at", "updated_at") 
WHERE "table_obj"."key1" = _data."key1" AND "table_obj"."key2" = _data."key2"

変数名でバインド

?変数名 の形式で記載することで可能です。
こちらのQueryメソッド のExampleがわかりやすいです。

リトライ・タイムアウト

クエリエラー時のリトライやクエリタイムアウトがDB接続時に設定するオプションで設定可能です。
リトライ対象とするエラーはこのあたりに定義されてます。

オプションで細かにリトライやタイムアウトが設定できるのはRDBMSを一つに絞ったことによってできたことだと理解しています。

なお、Contextにも対応しています。

トレーシング

まだ試していないですが、db.AddQueryHook(pgotel.TracingHook{}) のような記載をするだけでクエリフックでOpenTelemetryとの連携ができるようです。
このあたりのドキュメントに記載ありです。

ストリーム処理

ForEach を利用することでSELECT結果をストリーム的に処理できます。
以下、READMEより抜粋。

ForEach that calls a function for each row returned by the query without loading all rows into the memory.

例文そのままを以下に記載します

err := pgdb.Model((*Book)(nil)).
    OrderExpr("id ASC").
    ForEach(func(b *Book) error {
        fmt.Println(b)
        return nil
    })
if err != nil {
    panic(err)
}
//Book<Id=1 Title="book 1">
//Book<Id=2 Title="book 2">
//Book<Id=3 Title="book 3">

gormにもFindInBatchesというメソッドがありましたが、FindInBatchesはOFFSETとLIMITを駆使して一定件数ずつ取得できるクエリを複数回投げていたので、go-pg/pgのForEachとは似て非なるものです。

尚、for rows.Next() {...}のループ内で処理すれば他のDBアクセスライブラリでもストリーム処理は可能です。

Postgres特化

細かくチェックはしてないですが、以下のようなPostgres専用機能にも対応しているようです。
array型、hstore型、ON CONFLICT句、COPY FROM/TO構文

注意点

Structにつけるタグ

PKのタグは"pk"では検出できず、",pk"のように,が必要です。
※結構ここでハマりました。
他のタグ情報はこちらにまとまっています。テーブル名もタグで指定できるみたいです。

type TableObj struct {
    Key1  string `pg:",pk"`
    Key2  string `pg:",pk"`
    Str1  string
    Time1 time.Time
    Num1  string `pg:"type:numeric(4,2)"`
}

余談ですが、小数の値を正確に扱いたい場合は、go側はstringで、DB側はnumericで扱うのがよいかと思います。

処理クエリの出力方法

以下のpgdebug.DebugHookAddQueryHookに仕込むことで実行クエリが出力されます。
※v10から対応しているよう

import "github.com/go-pg/pg/extra/pgdebug"

//....
    db := pg.Connect(&pg.Options{...})
    defer db.Close()

    db.AddQueryHook(pgdebug.DebugHook{
        Verbose:   true,
        EmptyLine: true,
    })

Verbose:falseに設定すれば、エラー時のみクエリを出力するように変わるそうです。
しかし、2021/1/1に私が確認した限りはエラーになった時のクエリ出力がうまく機能していません。
Issueを挙げてみたので改善されるといいな。。

代わりに以下のQueryHookを設定すれば出力されたので、参考にしてみてください。

type MyDebugHook struct {
    // pgdebug.DebugHookと同じ内容
    Verbose   bool
    EmptyLine bool
}

func (h MyDebugHook) BeforeQuery(ctx context.Context, evt *pg.QueryEvent) (context.Context, error) {
    // pgdebug.DebugHookと同じ内容
    q, err := evt.FormattedQuery()
    if err != nil {
        return nil, err
    }

    if evt.Err != nil {
        fmt.Printf("%s executing a query:\n%s\n", evt.Err, q)
    } else if h.Verbose {
        if h.EmptyLine {
            fmt.Println()
        }
        fmt.Println(string(q))
    }

    return ctx, nil
}

func (h MyDebugHook) AfterQuery(ctx context.Context, evt *pg.QueryEvent) error {
    // ここだけpgdebug.DebugHookと内容が異なる
    if evt.Err != nil {
        q, _ := evt.FormattedQuery()
        fmt.Printf("%s executing a query:\n%s\n", evt.Err, q)
    }
    return nil
}
/* 使い方
    db.AddQueryHook(MyDebugHook{
        Verbose:   false,
        EmptyLine: true,
    })
*/

締め

まだまだ実運用していないので、その辺の感触は不明ですが、かなり良さそうな気配がしています。
Postgres × Golang の組み合わせで新規開発を検討している方に参考にしていただければ幸いです。
これを機にgo-pg/pgの日本語記事も増えるといいな!

sumally
テクノロジーで「所有」のあり方をアップデートする
https://pocket.sumally.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away