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)
}
}
}
godynamo
- Transaction
vs
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
}
godynamo
- RETURNING
vs
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となります
godynamo
- ページング
vs
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)
}
}
godynamo
- aws.Config
の連携
vs
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