CockroachDBへのアクセスとDBマイグレーション

今日はアプリからCockroachDBにアクセスしてみたいと思います。ついでにDBマイグレーションにも触れます。

## DBコネクションの作成

DBへのアクセスには標準の`database/sql`パッケージを使います。使用するドライバーはPostgresのものを使います。
ということで`main.go`で以下のパッケージを読み込みます。

```diff:main.go
import (
        log "github.com/sirupsen/logrus"
        "github.com/utilitywarehouse/go-operational/op"
        "google.golang.org/grpc"
+
+       _ "github.com/lib/pq"
)
```

`store.go`ファイルを作成し、DBに接続するための関数`initDB()`を以下のように定義します。

```go:store.go
package main

import (
	"database/sql"

	"github.com/pkg/errors"
)

func initDB(connString string) (*sql.DB, error) {
	db, err := sql.Open("postgres", connString)
	if err != nil {
		return nil, err
	}

	if err := db.Ping(); err != nil {
		return nil, errors.Wrap(err, "ping")
	}
	return db, nil
}
```

`main.go`からの呼び出しはこんな感じです。

```go:main.go
func main() {
  
        ...
 
+       dbURL := app.String(cli.StringOpt{
+               Name:   "db-url",
+               Desc:   "cockroachdb url",
+               EnvVar: "DB_URL",
+               Value:  "postgresql://root@localhost:26257/test?sslmode=disable",
+       })
+
        app.Action = func() {
                log.WithField("git_hash", gitHash).Println("Hello, world")
 
+               db, err := initDB(*dbURL)
+               if err != nil {
+                       log.WithError(err).Fatalln("connect db")
+               }
+               defer db.Close()
+
                lis, err := net.Listen("tcp", net.JoinHostPort("", strconv.Itoa(*grpcPort)))
                if err != nil {
                        log.Fatalln("init gRPC server:", err)

                ...
```

では`app.yaml`で環境変数`DB_URL`を定義してリソースを更新し、起動時にエラーが出ないことを確認してみましょう。

```diff:app.yaml
         env:
         - name: SRV_PORT
           value: "8080"
+        - name: DB_URL
+          value: "postgres://qiita_advent_calendar_2019@cockroachdb-proxy:26257/qiita_advent_calendar_2019_db?sslmode=disable"
         ports:
         - containerPort: 8080
           name: srv
```

```sh
$ kubectl apply -f kubernetes/app.yaml
service/qiita-advent-calendar-2019 unchanged
deployment.apps/qiita-advent-calendar-2019 configured

$ kubectl -n qiita logs qiita-advent-calendar-2019-5bc6786c75-gbr5s qiita-advent-calendar-2019
time="2019-11-26T18:24:23Z" level=info msg="Hello, world" git_hash=bb1916d14726fbd4d3e98b333d54ccfad45f824d
```

特にエラーがないため接続できているようです。

## テーブルスキーマ定義

せっかくDBに接続できたので、テーブルを作成してみましょう。
`internal/schema`パッケージを切り、テーブルスキーマ定義用のファイル`sql.go`を作成します。

```go:internal/schema/sql.go
package schema

var schemas = map[int]string{
	0: `
BEGIN;

CREATE TABLE schema_version (
        id        SMALLSERIAL NOT NULL PRIMARY KEY,
        md_insert BIGINT      DEFAULT  EXTRACT(EPOCH FROM current_timestamp)::INT,
        md_update BIGINT      DEFAULT  0,
        md_curr   BOOL        DEFAULT  'true'
);

INSERT INTO schema_version VALUES (0);

COMMIT;
`,
}

```

先に`schema_version`テーブルを書いてしまいました。
僕のチームでは特別マイグレーション用のライブラリを使用しておらず、このテーブルでスキーマのバージョン管理をしてます。
マイクロサービスにより、一つ一つのデータベースの規模は小さく、スキーマが更新される頻度はモノリスのアプリケーションに比べて少ないため、今のところはこれで事足りているという状況です。

ではマイグレーション用の関数`Migrate()`をみていきましょう。
関数は`internal/schema/schema.go`に置いておきます。

```go:internal/schema/schema.go
package schema

import (
	"database/sql"

	"github.com/pkg/errors"
)

// Migrate sets the schema at the requested version
func Migrate(db *sql.DB, expectedVersion int) error {
	currentVersion, err := Version(db)
	if err != nil {
		return err
	}
	switch {
	case expectedVersion == currentVersion:
		return nil
	case expectedVersion < currentVersion:
		return errors.Errorf("schema migrate: invalid request, can not migrate backwards, current: %v, expected: %v", currentVersion, expectedVersion)
	case expectedVersion > currentVersion:
		for ; expectedVersion > currentVersion; currentVersion++ {
			if _, err := db.Exec(schemas[currentVersion+1]); err != nil {
				return errors.Wrap(err, "exec migration")
			}
		}
	}
	return nil
}

// Version returns the current schema version
func Version(db *sql.DB) (v int, err error) {
	if err := db.QueryRow("SELECT id FROM schema_version WHERE md_curr = true").Scan(&v); err != nil {
		if err.Error() == `pq: relation "schema_version" does not exist` {
			return -1, nil
		}
	}
	return v, errors.Wrap(err, "select version")
}
```

この関数では指定されたバージョンまでのSQLを順に適用します。

## store構造体の定義とマイグレーションの実行

では`store.go`ファイルに戻り、DBアクセス用のメソッドを定義する`store`構造体を定義し、その初期化処理時に上記の`Migrate()`関数を呼び出すようにしましょう。

```go:store.go
package main

import (
	"database/sql"

	"github.com/KentaKudo/qiita-advent-calendar-2019/internal/schema"
	"github.com/pkg/errors"
)

...

type store struct {
	db *sql.DB
}

func newStore(db *sql.DB, version int) (*store, error) {
	if err := schema.Migrate(db, version); err != nil {
		return nil, errors.Wrap(err, "migrate db schema")
	}

	return &store{db: db}, nil
}
```

`main.go`ではこの初期化関数を呼び出すだけです。

```go:main.go
package main

...

var (
	gitHash              = "overriden at compile time"
	defaultSchemaVersion = 0
)

...

func main() {

	...

	schemaVersion := app.Int(cli.IntOpt{
		Name:   "schema-version",
		Desc:   "schema version",
		EnvVar: "SCHEMA_VERSION",
		Value:  defaultSchemaVersion,
	})

	app.Action = func() {
		
        ...

		_, err = newStore(db, *schemaVersion)
		if err != nil {
			log.WithError(err).Fatalln("init store")
		}

        ...
}
```

さてでは先ほどと同様にアプリをデプロイしなおしてエラーが出ないことを確認してみましょう。

```sh
$ kubectl -n qiita scale --replicas=0 deployment qiita-advent-calendar-2019
deployment.extensions/qiita-advent-calendar-2019 scaled
$ kubectl -n qiita scale --replicas=1 deployment qiita-advent-calendar-2019
deployment.extensions/qiita-advent-calendar-2019 scaled

$ kubectl -n qiita logs qiita-advent-calendar-2019-5bc6786c75-lx4zk qiita-advent-calendar-2019
time="2019-11-26T19:09:37Z" level=info msg="Hello, world" git_hash=395149f7219511f2d72a09cd935e53f56fd29255
```

エラーが出ていません。マイグレーションが正常に終了したようです。
昨日同様に直接DBを覗き込んでテーブルが作成されていることを確かめてみましょう。

```sh
$ kubectl -n qiita exec -it cockroachdb-0 -- /cockroach/cockroach sql --url postgres://root@localhost:26257 --insecure
...
root@localhost:26257/defaultdb> use qiita_advent_calendar_2019_db;
SET

Time: 867.116µs

root@localhost:26257/qiita_advent_calendar_2019_db> show tables;
    table_name    
+----------------+
  schema_version  
(1 row)

Time: 6.743223ms

root@localhost:26257/qiita_advent_calendar_2019_db> show columns from schema_version;
  column_name | data_type | is_nullable |                          column_default                           | generation_expression |   indices   | is_hidden  
+-------------+-----------+-------------+-------------------------------------------------------------------+-----------------------+-------------+-----------+
  id          | INT       |    false    | unique_rowid()                                                    |                       | {"primary"} |   false    
  md_insert   | INT8      |    true     | extract('epoch':::STRING, current_timestamp():::TIMESTAMPTZ)::INT |                       | {}          |   false    
  md_update   | INT8      |    true     | 0:::INT                                                           |                       | {}          |   false    
  md_curr     | BOOL      |    true     | true                                                              |                       | {}          |   false    
(4 rows)

Time: 21.007097ms

root@localhost:26257/qiita_advent_calendar_2019_db> select * from schema_version;
  id | md_insert  | md_update | md_curr  
+----+------------+-----------+---------+
   0 | 1574795377 |         0 |  true    
(1 row)

Time: 73.755494ms
```

無事テーブルが作成されていていい感じです。

---

今日はちょっといろいろやりました。
駆け足になってしまいましたがこれでDBのセットアップは完了したので明日、POSTエンドポイントを実装したいと思います。