ぼくとCloud Spannerの100日戦争

[Google AppEngine 2Genのgo111 standardランタイム ベータ公開](https://cloud.google.com/appengine/docs/standard/go111/release-notes#october_10_2018)に伴って、それまでDatastoreとCloudSQL(MySQL5.7)で開発していたアプリケーションをSpannerでリプレースすることがあったので、その際にやったこと等をつらつらと書いていきます。

## 前提
- サーバーアプリケーションは原則としてAppEngineで動作する前提
- CIにはCloudBuildを使用する
- 開発者それぞれが個別に開発用のGCP Projectを利用している（AppEngineはProjectに１つなので）


## ローカルエミュレーターが無い
Cloud Spannerには現状ローカルエミュレーターが無いですが、ちょいとあれこれしたら割となんとかなりました。

### Datastore + MySQL版
Datastore + MySQLの場合、Datastoreはgcloud sdkにエミュレーターが存在するのでそれを使用、MySQLはDocker経由で各自のマシンで起動という具合でローカル環境を構築していました。
多分そんなに変わった構成じゃないかなと。

### Spanner版
素直にSpannerに接続するようにしています。
開発者レベルでの環境分離は、SpannerのデータベースをGCP Project名と同じにする　という素朴なルールで特に困ることなく回っています。
みんなで同じプロジェクトの同じインスタンスを使いまわして、データベースだけ分けるという具合です。

GAE上のGoアプリケーションからSpannerに接続する場合、例えば以下のようなコードになるのですが

```golang
package main

import (
    "fmt"

    "cloud.google.com/go/spanner"
    "google.golang.org/api/option"
)

func main() {
    ctx := context.Background()

    // 接続先
    // project instance databaseにはそれぞれの環境に合わせたものにする必要がある
    // プロジェクト名とインスタンス名は固定でなんとかなるが、データベース名が開発者ごとに違う変数になる
    dsn := fmt.Sprintf("projects/%s/instances/%s/databases/%s", "project", "instance", "database")

    tokenSource, _ := google.DefaultTokenSource(ctx, "https://www.googleapis.com/auth/spanner.admin")
    client, _ := spanner.NewClient(ctx, dsn, option.WithTokenSource(tokenSource))
}
```

この際、DSNを組み立てるためのデータベース名が開発者固有の変数として残ってしまいます。
環境変数でos.Getenvするというのが素朴で良かったんですが、app.yamlの数は減らしたい（開発者ごとにenv_variablesを作らないといけないのは嫌だ)という気持ちになり、<font color="Red">AppEngineにデプロイするアプリ書いてるんだから、みんなgcloud initはしてるっしょ！</font>という発想から、

- ローカルではgcloud sdkのコンフィグファイルをパースして、アクティブなプロジェクトをデータベース名として採用する
- AppEngineで動作している場合は、メタデータサーバーに問い合わせて自分がデプロイされているプロジェクトをデータベース名として採用する

というライブラリをエイヤッと書いて対応しています。

自分が今ローカル動作なのか、GAE動作なのかは、GAEはこちらから何も指定していなくても、以下のような環境変数を持っているので、このあたりを見て判断しています

```
GAE_MEMORY_MB=128
GAE_INSTANCE={インスタンスのID　GAEが勝手に決めてると思う}
PORT=8080
HOME=/root
GAE_SERVICE={デプロイしたサービス名 app.yamlのserviceに書いたもの}
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
GAE_DEPLOYMENT_ID={デプロイID GAEが勝手に決めてると思う}
DEBIAN_FRONTEND=noninteractive
GOOGLE_CLOUD_PROJECT={自分がデプロイされているプロジェクト}
GAE_ENV=standard
GAE_APPLICATION={b~の後ろにGOOGLE_CLOUD_PROJECTの中身がくっついたようなやつ}
PWD=/srv
GAE_RUNTIME=go111
GAE_VERSION=20181031t131554
```

~~これ書いてて思いましたけど、メタデータサーバーまで行かなくてもGOOGLE_CLOUD_PROJECTの値見れば良い感ありますね。そっと修正しておこう。~~

もうちょっといいやり方あれば誰か教えてほしいです。

## ユニットテストに組み込みたい

### Datastore + MySQL版
Datastore Emulatorはなんかよしなに繋がるので特に意識せずに使ってました。
MySQLもDockerでホスト側ポートが全員同じポートで固定されていたので、そこに素直にアクセスしていました

### Spanner版
[大変詳しい記事があったのでこちらを参考に](https://qiita.com/castaneai/items/c49b9c3c49a7ccc060dc)していただけたら良いと思います。
私もだいたい同じ感じで、テストケース毎にSpannerのDBを使い捨てています。

```golang
func TestABC(t *testing.T) {
    databaseInit()
    defer databaseDrop()

    // テストケースつらつら書いていく
}
```
要するにこんな感じです。
ユニットテストの場合はDB名は各テストが走るたびにUUIDベースで生成して、30文字で切り捨てるようなことをやっています。（Spannerのデータベースは最大で30文字なので)

デバッガから落としたりするとdeferが実行されずにDROPが走りませんが、開発用のSpannerインスタンスは毎晩自動で落とすようにしているので、その際データベースも消えるしまぁいっかな　というお気楽運用にしています。

## データベースのセットアップが終わらない
### Datastore + MySQL版
MySQLのマイグレーションに[go-migrate](https://github.com/golang-migrate/migrate)というライブラリを使用していました。

### Spanner版
go-migrateはspannerにも対応しているのですが、これがパフォーマンス的にあまり良くなかったです。
[大変詳しい記事があったのでこちらを参考に](https://qiita.com/castaneai/items/c49b9c3c49a7ccc060dc#extrastatements-%E3%82%92%E4%BD%BF%E3%81%A3%E3%81%A6%E3%83%86%E3%83%BC%E3%83%96%E3%83%AB%E3%81%A8%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90%E3%82%92%E7%88%86%E9%80%9F%E3%81%AB%E3%81%99%E3%82%8B)していただけるとよく分かるのですが、SpannerはCREATE DATABASE作成と同時にDDLを渡してやらないと、CREATE TABLEするだけでもかなりの時間がかかります。
go-migrateはDDLを一気に渡すような挙動ではなかったため、このあたりは開発用に簡単なコマンドを作成して対処しています。
といっても、上に書いたような判断で接続先のデータベースを勝手に見分けて、所定の位置からDDLとDMLをかき集めて実行しているだけなのですが。

ユニットテストの場合は、databaseInit的な関数の中身がCREATE DATABASEと同時にDDLを投げつけるように実装してある感じです。