はじめに
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ライブラリってあるのかなと調査したのが、そもそもの発端でした。
- SELECTはRawSQLで書きたい(ORMは読みにくい)
- RawSQLでは変数名でバインドしたい
- Insert, Update, DeleteはORMでやりたい(SQLだと冗長)
- 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.DebugHook
をAddQueryHook
に仕込むことで実行クエリが出力されます。
※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の日本語記事も増えるといいな!