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

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

go-pg/pgの日本語記事の第一号です！！（自分調べ）


# [go-pg/pg](https://pg.uptrace.dev/)とは
名前からもお察しの通り、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は読みにくい）
1. RawSQLでは変数名でバインドしたい
1. Insert, Update, DeleteはORMでやりたい（SQLだと冗長）
1. Insert, Update, DeleteはBulkクエリをしたい（1クエリで複数レコード処理）

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

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

## Bulk処理
* go-pg/pgはBulkInsert, BulkUpdate, BulkDelete、全て可能です。
* 例えばBulkUpdateのクエリは以下のようになります。PKでレコードを一意に特定した上での1クエリになっています。
* ちなみに、gormはBulkInsertのみ対応してます。他のライブラリも私の知る限りBulkUpdateが対応しているものはないです。

```sql
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メソッド](https://pkg.go.dev/github.com/go-pg/pg/v10#DB.Query) のExampleがわかりやすいです。

## リトライ・タイムアウト
クエリエラー時のリトライやクエリタイムアウトが[DB接続時に設定するオプション](https://pkg.go.dev/github.com/go-pg/pg/v10#DB.Query)で設定可能です。
リトライ対象とするエラーは[このあたり](https://github.com/go-pg/pg/blob/v10.7.3/base.go#L180-L206)に定義されてます。

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

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

## トレーシング
まだ試していないですが、`db.AddQueryHook(pgotel.TracingHook{})` のような記載をするだけでクエリフックでOpenTelemetryとの連携ができるようです。
[このあたりのドキュメント](https://pg.uptrace.dev/tracing/)に記載ありです。

## ストリーム処理
`ForEach` を利用することでSELECT結果をストリーム的に処理できます。
以下、READMEより抜粋。
> ForEach that calls a function for each row returned by the query without loading all rows into the memory.

[例文そのままを以下に記載します]((https://pkg.go.dev/github.com/go-pg/pg/v10#example-DB.Model-ForEach))。

```go
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というメソッド](https://gorm.io/docs/advanced_query.html#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"`のように`,`が必要です。
※結構ここでハマりました。
他のタグ情報は[こちら](https://pg.uptrace.dev/models/)にまとまっています。テーブル名もタグで指定できるみたいです。

```go
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から対応しているよう

```go
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](https://github.com/go-pg/pg/issues/1805)を挙げてみたので改善されるといいな。。

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

```go
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の日本語記事も増えるといいな！