今日はアプリからCockroachDBにアクセスしてみたいと思います。ついでにDBマイグレーションにも触れます。
DBコネクションの作成
DBへのアクセスには標準のdatabase/sql
パッケージを使います。使用するドライバーはPostgresのものを使います。
ということで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()
を以下のように定義します。
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
からの呼び出しはこんな感じです。
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
を定義してリソースを更新し、起動時にエラーが出ないことを確認してみましょう。
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
$ 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
を作成します。
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
に置いておきます。
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()
関数を呼び出すようにしましょう。
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
ではこの初期化関数を呼び出すだけです。
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")
}
...
}
さてでは先ほどと同様にアプリをデプロイしなおしてエラーが出ないことを確認してみましょう。
$ 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を覗き込んでテーブルが作成されていることを確かめてみましょう。
$ 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エンドポイントを実装したいと思います。