Go: DynamoDB(PartiQL)用のDBドライバを実装してみた
はじめに
今年の春頃からmiyamo2/dynmgrmというGORMドライバの開発を始めました
miyamo2/dynmgrmではDBドライバにgodynamoを使用していますが
ちょっとワケあってv0.9.0からはフォーク版のmiyamo2/godynamoに切り替えています
今後どうせメンテしていくのであれば...と思い
0ベースでぼくの考える最強PartiQLドライバを作るに至りました
作ったもの
余談ですがpgxに倣ってpqxとしたかったものの
字面があまりにpgxと似すぎているのでpqxdとしました
サンプルコード
package main
import (
"context"
"database/sql"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/miyamo2/pqxd"
"log"
"time"
)
func main() {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
db := sql.OpenDB(pqxd.NewConnector(cfg))
if db == nil {
log.Fatal(err)
}
if err := db.Ping(); err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, `SELECT id, name FROM "users"`)
if err != nil {
log.Fatalf("something happend. err: %s\n", err.Error())
return
}
for rows.NextResultSet() { // page feed with next token
for rows.Next() {
var (
id string
name string
)
if err := rows.Scan(&id, &name); err != nil {
fmt.Printf("something happend. err: %s\n", err.Error())
continue
}
log.Printf("id: %s, name: %s\n", id, name)
}
}
}
vs godynamo - Transaction
godynamo
godynamoではTransaction内でSELECTを使用することができません
pqxd
pqxdではPK(, SK)を使用した結果が一意になるクエリ(GetItem)に限り使用することができます
tx, err := db.Begin()
if err != nil {
return err
}
ctx := context.Background()
row := tx.QueryRowContext(ctx, `SELECT id, name FROM "users" WHERE id = ?`, "1")
if err != nil {
tx.Rollback()
return err
}
var (
id string
name string
)
if err := row.Scan(&id, &name); err != nil {
fmt.Printf("something happend. err: %s\n", err.Error())
return
}
fmt.Printf("id: %s, name: %s\n", id, name)
row := tx.QueryRowContext(ctx, `SELECT id, name FROM "users" WHERE id = ?`, "1")
var (
id string
name string
)
if err := row.Scan(&id, &name); err != nil { // ここでTransactionがCommitされる
fmt.Printf("something happend. err: %s\n", err.Error())
return
}
row := tx.QueryRowContext(ctx, `SELECT id, name FROM "users" WHERE id = ?`, "2")
// このクエリはTransaction外で実行される
if err := row.Scan(&id, &name); err != nil {
fmt.Printf("something happend. err: %s\n", err.Error())
return
}
vs godynamo - RETURNING
godynamo
godynamoでもサポートはされていますがこの機能にはスキーマレスDB特有の課題があります
row := db.QueryRowContext(
context.Background(),
`UPDATE "users" SET name = ? SET nickname = ? WHERE id = ? RETURNING MODIFIED OLD *`,
"Robert",
"Bob",
"2",
)
var id, name, nickname string
if err := row.Scan(&id, &nickname, &name); err != nil {
fmt.Printf("something happend. err: %s\n", err.Error())
return
}
DynamoDBではPK(,SK)以外の項目は全て未定義になりえます
イメージとしては下記のような状態です
id(PK) |
name(GSI-PK) |
nickname |
disabled |
|---|---|---|---|
| 1 | 宮本太郎 |
miyamo2 |
|
| 2 | 宮本花子 |
||
| 3 | |
||
| 4 | NULL | true |
ではRETURNING *の結果を取得する際、
row.Scanに渡す変数の数や順番はどのようにするのが正解でしょうか?
...答えとしては分からない(itemによって変動する)になると思います
pqxd
pqxdではこの課題へのアプローチとしてRETURNING句でのカラム指定をサポートしています
row := db.QueryRowContext(
context.Background(),
`UPDATE "users" SET name = ? SET nickname = ? WHERE id = ? RETURNING MODIFIED OLD name, nickname, disabled`,
"Robert",
"Bob",
"2",
)
var (
name sql.NullString
nickname sql.NullString
disabled sql.NullBool
)
if err := row.Scan(&name, &nickname, &disabled); err != nil {
fmt.Printf("something happend. err: %s\n", err.Error())
return
}
if name.Valid {
fmt.Printf("name: %s\n", name.String)
}
if nickname.Valid {
fmt.Printf("nickname: %s\n", nickname.String)
}
if disabled.Valid {
fmt.Printf("disabled: %v\n", disabled.Bool)
}
取得するカラムやその順番が固定され、仮に項目が未定義だった場合はnilとなります
vs godynamo - ページング
godynamo
自動で最後までフェッチしたうえでResultSetを返します
pqxd
NextResultSetによって再取得を行います
そのためループ途中でerrorが発生し早期リターンなどを行う場合は
DynamoDBへクエリを発行する回数をgodynamoより減らせる可能性があります
for rows.NextResultSet() { // 初回は何もしない、二回目以降は残りのResultSetを取得
for rows.Next() {
var (
id string
name string
)
if err := rows.Scan(&id, &name); err != nil {
return err
}
if err := RelativeValidate(name); err != nil {
return err
}
fmt.Printf("id: %s, name: %s\n", id, name)
}
}
vs godynamo - aws.Configの連携
godynamo
godynamoの場合RegisterAWSConfigを利用してaws.Configを登録することができます
package main
import (
"database/sql"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/btnguyen2k/godynamo"
)
func init () {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
godynamo.RegisterAWSConfig(cfg)
}
func main() {
driver := "godynamo"
db, err := sql.Open("godynamo", "")
if err != nil {
log.Fatal(err)
}
}
RegisterAWSConfigで登録されたaws.Configはsync.RWMutexによって排他制御はされているものの
driver.Openの度に参照されるため、アプリケーション起動後に書き換えを行ってしまうと
以前・以降に作られたコネクションが参照するaws.Configに差異が出てしまいます
これはgodynamoによって生成された全てのsql.DBに影響するため
下記のような使い方はできません
godynamo.RegisterAWSConfig(cfg)
db1, _ := sql.Open("godynamo", "")
cfg = LoadAnotherConfig()
godynamo.RegisterAWSConfig(cfg)
db2, _ := sql.Open("godynamo", "")
pqxd
pqxdではaws.ConfigでDynamoDBと接続するためのdriver.Connectorを提供しているため
よりシンプル、かつ安全にaws.Configを連携することができます
package main
import (
"context"
"database/sql"
"fmt"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/miyamo2/pqxd"
"log"
"time"
)
func main() {
ctx := context.Background()
cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("ap-northeast-1"))
if err != nil {
log.Fatalf("unable to load SDK config, %v", err)
}
db := sql.OpenDB(pqxd.NewConnector(cfg))
if db == nil {
log.Fatal("failed to get db")
}
}
pqxdでまだできないこと
godynamoでサポートしている機能のうち
CREATE TABLEなどのDDLの実装が現状間に合っていません
PartiQL in DynamoDB自体はCRUD以外の構文を提供していないので
DQLの構文を採用する予定です
Discussion