【Cloud Spanner】yoを使ってスキーマからGoの構造体を生成する

# はじめに

本記事では、Cloud Spanner用のコード生成ツールである yo について説明します。

# Spannerについて

Cloud Spannerは、Google が提供するフルマネージド型の分散データベースです。従来のリレーショナルデータベース（SQL）のトランザクション正確性と整合性を維持しつつ、NoSQL データベースのように水平方向へのスケーラビリティを持つ NewSQL に該当します。

https://cloud.google.com/spanner?hl=ja

# **yoについて**

yoは[Cloud Spanner](https://github.com/xo/xo)のスキーマ定義から Go のコードを自動生成するツールで、次の特徴があります。

- Go の構造体をスキーマから生成
- CRUD 操作のための関数を生成
    - CUD については、Mutation を返します
- セカンダリーインデックスに対応した取得関数を生成
    - FindTableNameByIndexName みたいなメソッドが生成されます

ただし、JOINなどの複雑なクエリは生成されません。そのため、必要に応じて手動でクエリを書く必要があります。

https://github.com/cloudspannerecosystem/yo

# 環境

- go v1.22
- yo v0.5.7

GCPのプロジェクトやCloud Spannerのインスタンス、データベースは既に作成済みとします。

Cloud Spannerの操作方法は以下をご覧ください。

https://cloud.google.com/spanner/docs/create-query-database-console?hl=ja

# **インストール方法**

次のコマンドで yo をインストールできます。

```bash
go get -u go.mercari.io/yo
```

# **スキーマを定義**

Cloud Spannerのデータベースでスキーマを定義する必要があります。以下は、ユーザー情報を格納するテーブルのシンプルなスキーマ定義の例です。Name と Email にインデックスを貼っています。

```sql
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 ディレクトリ内にコードが生成されます。

```bash
.
├── go.mod
├── go.sum
├── main.go
├── schema.sql
└── yo
```

以下のコマンドを実行し、データベースのスキーマから Go のコードを生成します。

```bash
yo generate ./schema.sql --from-ddl -o ./yo
```

また、プロジェクトやインスタンス名を指定して生成することも可能です。

```bash
yo $SPANNER_PROJECT_NAME $SPANNER_INSTANCE_NAME $SPANNER_DATABASE_NAME -o ./yo
```

サフィックスに yo とついた `yo_db.yo.go`, `user.yo.go` ファイルが生成されていると思います。

# **コード解説**

`user.yo.go` を見てみます。

```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`についても同様です）

```go
func (u *User) Insert(ctx context.Context) *spanner.Mutation {
...
}
```

 SpannerにはMutationとDMLの二つのデータ操作の手段が用意されていますが、yoで扱うのはMutationになります。

https://cloud.google.com/spanner/docs/dml-versus-mutations?hl=ja#feature_comparison_between_dml_and_mutations

また、プライマリキーで取得するための関数やセカンダリインデックスで取得するための関数が定義されています。

```go
func FindUser(ctx context.Context, db YORODB, userID string) (*User, error) {
...
}
```

インデックスにユニーク制約をつけることによって、複数取得ではなく単数で値が返ってきます。

```go
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`を受け取れるよになっています。

```go
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を作成します。

```go
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`を実行する必要があります。

```go
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`の第二引数に渡す必要があります。

```go
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を操作する方法をまとめました。最後まで読んでいただきありがとうございます。