[GCP, Go]Spanner公式ライブラリのまとめ

ざっくり思い出すときの自分用メモ。詳細はリファレンスを参照。

# 1. DBのCREATE, DROP, LIST, DBスキーマの更新
- `google-cloud-go.spanner.admin.database.apiv1` パッケージ の `DatabaseAdminClient` を使う

DBを作成後、テーブルも即座に作成する[公式のサンプルコード](https://cloud.google.com/spanner/docs/getting-started/go?hl=ja#create_a_database) 。
コメント付与してみた。
```sample.go
import (
        "context"
        "fmt"
        "io"
        "regexp"

        database "cloud.google.com/go/spanner/admin/database/apiv1"
        adminpb "google.golang.org/genproto/googleapis/spanner/admin/database/v1"
)

// 引数dbは、`projects/<project>/instances/<instance>/databases/<database>`というフォーマットになっていることを期待している
func createDatabase(ctx context.Context, w io.Writer, db string) error {
        // 引数のフォーマットチェック
        matches := regexp.MustCompile("^(.*)/databases/(.*)$").FindStringSubmatch(db)
        if matches == nil || len(matches) != 3 {
                return fmt.Errorf("Invalid database id %s", db)
        }

        // Cloud Spanner Database Admin API (DBのCREATE, DROP, LIST, 既存のDBスキーマの更新ができる)を叩くためのクライアントを生成
        // https://pkg.go.dev/google.golang.org/cloud/spanner/admin/database/apiv1#NewDatabaseAdminClient
        adminClient, err := database.NewDatabaseAdminClient(ctx)
        if err != nil {
                return err
        }
        defer adminClient.Close()

        // op CreateDatabaseOperation は、CreateDatabaseに伴う長時間の実行操作を管理するもの
        // https://pkg.go.dev/google.golang.org/cloud/spanner/admin/database/apiv1#CreateDatabaseOperation
        // CreateDataBaseRequest
        //   Parent: 必須パラメータ。DBを作成するSpannerインスタンス名.`projects/<project>/instances/<instance>`というフォーマットが求められる。
        //   CreateStatement: 必須パラメータ。作成するDBのIDを指定したCREATE DATABASE文。IDはバッククォートで囲まれていなければならない。
        //   ExtraStatements: 任意パラメータ。DB作成後に実行するDDL文のリスト。DDL文ではテーブル, インデックス等の作成ができる。DDL文にエラーがある場合、DBは作成されない。
        // https://pkg.go.dev/cloud.google.com/go/spanner/admin/database/apiv1/databasepb#CreateDatabaseRequest
        op, err := adminClient.CreateDatabase(ctx, &adminpb.CreateDatabaseRequest{
                Parent:          matches[1],
                CreateStatement: "CREATE DATABASE `" + matches[2] + "`",
                ExtraStatements: []string{
                        `CREATE TABLE Singers (
                                SingerId   INT64 NOT NULL,
                                FirstName  STRING(1024),
                                LastName   STRING(1024),
                                SingerInfo BYTES(MAX),
                                FullName   STRING(2048) AS (
                                        ARRAY_TO_STRING([FirstName, LastName], " ")
                                ) STORED
                        ) PRIMARY KEY (SingerId)`,
                        `CREATE TABLE Albums (
                                SingerId     INT64 NOT NULL,
                                AlbumId      INT64 NOT NULL,
                                AlbumTitle   STRING(MAX)
                        ) PRIMARY KEY (SingerId, AlbumId),
                        INTERLEAVE IN PARENT Singers ON DELETE CASCADE`,
                },
        })
        if err != nil {
                return err
        }

        // CreateDataBaseOperation.Wait(ctx)は、長時間の実行操作が完了するまで待つもの
        // https://pkg.go.dev/google.golang.org/cloud/spanner/admin/database/apiv1#CreateDatabaseOperation.Wait
        if _, err := op.Wait(ctx); err != nil {
                return err
        }
        fmt.Fprintf(w, "Created database [%s]\n", db)
        return nil
}
```

# 2. DBへのデータの読み書き、トランザクションの実行
- [`google-cloud-go.spanner`パッケージ](https://pkg.go.dev/cloud.google.com/go/spanner) の [`Client`](https://pkg.go.dev/cloud.google.com/go/spanner#Client)構造体を起点に、データの読み取り、書き込み(`INSERT, UPDATE, DELETE`)ができる。
- 書き込みを伴うトランザクション
    - [`ReadWriteTransaction`](https://pkg.go.dev/cloud.google.com/go/spanner#ReadWriteTransaction)を利用する
    - 読み取り操作は、「テーブル, レコードのキー, レコードのカラム」, 「DML」どちらかを利用したメソッドで行う。
    - 書き込み操作は、「ミューテーション」,「DML」どちらかを利用したメソッドで行う(詳細は後述)。
        - [ミューテーション](https://cloud.google.com/spanner/docs/dml-versus-mutations?hl=ja#mutations-concept)
            - Spannerによって不可分で適用される一連の`INSERT, UPDATE, DELETE`操作。
        - 不可分操作(アトミック操作)
            - 途中で中断したり、部分的に実行したりできない操作。
- 読み取りのみのトランザクション
    - [`ReadOnlyTransaction`](https://pkg.go.dev/cloud.google.com/go/spanner#ReadOnlyTransaction)を利用する。
    - 読み取り操作は、「テーブル, レコードのキー, レコードのカラム」, 「DML」どちらかを利用したメソッドで行う。

## 2.1. 書き込みを伴うトランザクション(ReadWriteTransaction利用)
### 複数回の読み取り、書き込み実行([ReadWriteTransacitonメソッド](https://pkg.go.dev/cloud.google.com/go/spanner#Client.ReadWriteTransaction)利用)
```sample.go
_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
    // 引数のfuncの引数txnにclient.ReadTransaction()の内部処理で発行するトランザクションが渡される形になる
    // 内部でReadWriteTransaction構造体の読み取りメソッド(Read, Queryなど)、書き込みメソッド(BufferWrite, Updateなど)を複数回実行できる
    iter := txn.Read(...)
    ...
    err := txn.BufferWrite(...)    
    if err != nil {
        // Handle error.
	}
    ...
})
if err != nil {
    // Handle error.
}
```

### 読み取りなしでMutation群による書き込みを1回だけ実行([Applyメソッド](https://pkg.go.dev/cloud.google.com/go/spanner#Client.Apply)利用)
```sample.go
    client, err := spanner.NewClient(...)
	if err != nil {
		// TODO: Handle error.
	}
	m1 := spanner.Update(...)
    m2 := spanner.Update(...)
    // Applyは内部的にはReadWriteTransactionを利用している
	_, err = client.Apply(ctx, []*spanner.Mutation{m1, m2})
	if err != nil {
		// TODO: Handle error.
	}
 ```

### ReadWriteTransactionの読み取り、書き込みメソッドの例
```sample.go
// テーブル、レコードのキー群、取得するレコードのカラム群を指定して読み込み
func (t *ReadOnlyTransaction) Read(ctx context.Context, table string, keys KeySet, columns []string) *RowIterator
// DML文を指定して読み込み
func (t *ReadOnlyTransaction) Query(ctx context.Context, statement Statement) *RowIterator

// Mutation群を指定して書き込み
func (t *ReadWriteTransaction) BufferWrite(ms []*Mutation) error
// DML文を指定して書き込み
func (t *ReadWriteTransaction) Update(ctx context.Context, stmt Statement) (rowCount int64, err error)
```

### 補足: ミューテーション と DML
- [機能比較](https://cloud.google.com/spanner/docs/dml-versus-mutations?hl=ja#feature_comparison_between_dml_and_mutations)
    - DMLでは書込み後の読み取りができるが、ミューテーションでは不可能
    - DMLではupsertができないが、ミューテーションでは可能
    - DMLでは各DML文の後に制約チェック、ミューテーションではコミット時に制約チェック
- [ベストプラクティス: DML と ミューテーション は実行順序を考慮する必要がないよう、同じトランザクション内で混在させないこと](https://cloud.google.com/spanner/docs/dml-versus-mutations?hl=ja#best_practice_-_avoid_mixing_dml_and_mutation_in_the_same_transaction)
- [ミューテーションのほうが高速らしい](https://medium.com/google-cloud/cloud-spanner-latency-comparison-between-dml-and-mutations-ba32409ffe0f)


## 2.2. 読み取りのみのトランザクション(ReadOnlyTransaction利用)
### 複数回の読み取りを実行([ReadOnlyTransactionメソッド](https://pkg.go.dev/cloud.google.com/go/spanner#Client.ReadOnlyTransaction)利用)
```sample.go
	txn := client.ReadOnlyTransaction()
	defer txn.Close()

    // ReadOnlyTransactionのメソッド(Read, Query など)を複数回実行
    iter := txn.Read(...)
    ...
    iter := txn.Read(...)
    ...
```

### 一回だけ読み取りを実行([Singleメソッド](https://pkg.go.dev/cloud.google.com/go/spanner#Client.Single)利用)
```sample.go
    // Client.Single
    // Single は、単一の読み取りまたはクエリのみが必要な場合に最適化された読み取り専用のスナップショット・トランザクションを提供する。
    // これは、単一の読み取りや問い合わせに対して ReadOnlyTransaction() を使用するよりも効率的である。
    // https://pkg.go.dev/cloud.google.com/go/spanner#Client.Single
    tx  := client.Single()

    // ReadOnlyTransactionのメソッド(Read, Query など)を1回だけ実行
    iter := txn.Read(...)
    ...
```

### ステイル読み取りを実行(複数回読み取りの例)([WithTimestampBoundメソッド](https://pkg.go.dev/cloud.google.com/go/spanner#ReadOnlyTransaction.WithTimestampBound)利用)
```sample.go
	txn := client.ReadOnlyTransaction().WithTimestampBound(spanner.ExactStaleness(15 * time.Second))
	defer txn.Close()

    // ReadOnlyTransactionのメソッド(Read, Query など)を複数回実行
    iter := txn.Read(...)
    ...
    iter := txn.Read(...)
    ...
```

### ReadOnlyTransactionの読み取りメソッドの例
```sample.go
// テーブル、レコードのキー群、取得するレコードのカラム群を指定して実行
func (t *ReadOnlyTransaction) Read(ctx context.Context, table string, keys KeySet, columns []string) *RowIterator

// DML文を指定して実行
func (t *ReadOnlyTransaction) Query(ctx context.Context, statement Statement) *RowIterator
```

## 補足:トランザクションの使い分け
- [Cloud Spanner における各種トランザクションの使い分け](https://zenn.dev/google_cloud_jp/articles/15d34df66becfe)にきれいにまとめられている。素晴らしい・・・

# 3. その他用語
## DDL と DML
- DDL: Data Definition Language。SQLの `CREATE`, `DROP`, `ALTER`, `TRUNCATE`。根本的なデータ構造に対する操作をするものたち。
- DML: Data Manipulation Language。SQLの `INSERT`, `UPDATE`, `DELETE`, `SELECT`。テーブルに対する操作をするものたち。

## 二相コミット
- トランザクション処理を準備フェーズ、確定フェーズの2つに分けて行う手法。以下ざっくり
- 準備フェーズ
    - 処理対象のデータに対してロックを取得
    - 変更が制約(一意性制約や外部キー制約など)を満たしているか検証
    - それぞれの工程で失敗した場合はロールバックされる
- 確定フェーズ
    - データの変更をデータベースへ反映。失敗した場合はロールバックされる