はじめに
本記事では、Cloud Spanner用のコード生成ツールである yo について説明します。
Spannerについて
Cloud Spannerは、Google が提供するフルマネージド型の分散データベースです。従来のリレーショナルデータベース(SQL)のトランザクション正確性と整合性を維持しつつ、NoSQL データベースのように水平方向へのスケーラビリティを持つ NewSQL に該当します。
yoについて
yoはCloud Spannerのスキーマ定義から Go のコードを自動生成するツールで、次の特徴があります。
- Go の構造体をスキーマから生成
- CRUD 操作のための関数を生成
- CUD については、Mutation を返します
- セカンダリーインデックスに対応した取得関数を生成
- FindTableNameByIndexName みたいなメソッドが生成されます
ただし、JOINなどの複雑なクエリは生成されません。そのため、必要に応じて手動でクエリを書く必要があります。
環境
- go v1.22
- yo v0.5.7
GCPのプロジェクトやCloud Spannerのインスタンス、データベースは既に作成済みとします。
Cloud Spannerの操作方法は以下をご覧ください。
インストール方法
次のコマンドで yo をインストールできます。
go get -u go.mercari.io/yo
スキーマを定義
Cloud Spannerのデータベースでスキーマを定義する必要があります。以下は、ユーザー情報を格納するテーブルのシンプルなスキーマ定義の例です。Name と Email にインデックスを貼っています。
CREATE TABLE User (
UserId STRING(MAX) NOT NULL,
Name STRING(MAX) NOT NULL,
Email STRING(MAX) NOT NULL,
CreatedAt TIMESTAMP NOT NULL,
UpdatedAt TIMESTAMP NOT NULL,
) PRIMARY KEY (UserId);
CREATE INDEX Idx_User_Name ON User(Name);
CREATE UNIQUE INDEX Idx_User_Email ON User(Email);
Spanner のデータベース言語には PostgreSQL と Google SQL の2種類がありますが、今回は Google SQL を使用しています。コード生成を行う際に yo ではデータベース言語を指定できなかったため、Google SQLのみが対応している可能性があります。詳しい方がいらっしゃいましたら、ぜひ教えていただきたいです。
自動生成
スキーマが定義されたら、yoを使用してコードを自動生成します。
次のようなフォルダ構成になっており、yo ディレクトリ内にコードが生成されます。
.
├── go.mod
├── go.sum
├── main.go
├── schema.sql
└── yo
以下のコマンドを実行し、データベースのスキーマから Go のコードを生成します。
yo generate ./schema.sql --from-ddl -o ./yo
また、プロジェクトやインスタンス名を指定して生成することも可能です。
yo $SPANNER_PROJECT_NAME $SPANNER_INSTANCE_NAME $SPANNER_DATABASE_NAME -o ./yo
サフィックスに yo とついた yo_db.yo.go
, user.yo.go
ファイルが生成されていると思います。
コード解説
user.yo.go
を見てみます。
type User struct {
UserID string `spanner:"UserId" json:"UserId"` // UserId
Name string `spanner:"Name" json:"Name"` // Name
Email string `spanner:"Email" json:"Email"` // Email
CreatedAt time.Time `spanner:"CreatedAt" json:"CreatedAt"` // CreatedAt
UpdatedAt time.Time `spanner:"UpdatedAt" json:"UpdatedAt"` // UpdatedAt
}
定義したスキーマに対応する構造体が定義されていることがわかります。User
のメソッドとしてMutation
を返す Insert
メソッドが定義されています。(Update
やDelete
についても同様です)
func (u *User) Insert(ctx context.Context) *spanner.Mutation {
...
}
SpannerにはMutationとDMLの二つのデータ操作の手段が用意されていますが、yoで扱うのはMutationになります。
また、プライマリキーで取得するための関数やセカンダリインデックスで取得するための関数が定義されています。
func FindUser(ctx context.Context, db YORODB, userID string) (*User, error) {
...
}
インデックスにユニーク制約をつけることによって、複数取得ではなく単数で値が返ってきます。
func FindUserByEmail(ctx context.Context, db YORODB, email string) (*User, error) {
...
}
func FindUsersByName(ctx context.Context, db YORODB, name string) ([]*User, error) {
...
}
Nameの場合、ユニーク制約をつけていないので戻り値がスライスになっていますね。
yo_db.yo
ファイルにYORODB
が定義されており、Spannerのクライアントが発行したReadWriteTransaction
または ReadOnlyTransaction
を受け取れるよになっています。
type YORODB interface {
ReadRow(ctx context.Context, table string, key spanner.Key, columns []string) (*spanner.Row, error)
Read(ctx context.Context, table string, keys spanner.KeySet, columns []string) *spanner.RowIterator
ReadUsingIndex(ctx context.Context, table, index string, keys spanner.KeySet, columns []string) (ri *spanner.RowIterator)
Query(ctx context.Context, statement spanner.Statement) *spanner.RowIterator
}
使ってみる
以下では、yoで生成されたコードを使用してSpannerを操作します。
まず、Spanner Clientを作成します。
type DBConfig struct {
ProjectID string
InstanceName string
DBName string
}
func NewClient(ctx context.Context, cfg DBConfig) (*spanner.Client, error) {
fullDBName := fmt.Sprintf("projects/%s/instances/%s/databases/%s", cfg.ProjectID, cfg.InstanceName, cfg.DBName)
client, err := spanner.NewClient(ctx, fullDBName)
if err != nil {
return nil, err
}
return client, nil
func main() {
ctx := context.Background()
cfg := DBConfig{
ProjectID: "project-id",
InstanceName: "instance-name",
DBName: "database-name",
}
client, err := NewClient(ctx, cfg)
if err != nil {
fmt.Println(err)
return
}
defer client.Close()
}
yoで生成されたコードを使用してユーザーを作成します。Insert
は*spanner.Mutation
を返すため、最後にtx.BufferWrite
を実行する必要があります。
func main() {
省略...
user := &yo.User{
UserID: "user1",
Name: "John Doe",
Email: "john@example.com",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
f := func(ctx context.Context, tx *spanner.ReadWriteTransaction) error {
muts := []*spanner.Mutation{
user.Insert(ctx),
}
return tx.BufferWrite(muts)
}
_, err = client.ReadWriteTransaction(ctx, f)
if err != nil {
fmt.Println(err)
return
}
取得したい場合は、client
から ReadWriteTransaction
または ReadOnlyTransaction
を作成し、FindUser
の第二引数に渡す必要があります。
func main() {
省略...
tx := client.ReadOnlyTransaction()
user, err = yo.FindUser(ctx, tx, "user1")
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("User: %+v\n", user)
}
まとめ
今回はCloud Spanner用のコード生成ツールであるyo用いて、スキーマからコードを生成しSpannerを操作する方法をまとめました。最後まで読んでいただきありがとうございます。