0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

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

DBコネクションの作成

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

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()を以下のように定義します。

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からの呼び出しはこんな感じです。

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を定義してリソースを更新し、起動時にエラーが出ないことを確認してみましょう。

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

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に置いておきます。

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()関数を呼び出すようにしましょう。

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ではこの初期化関数を呼び出すだけです。

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エンドポイントを実装したいと思います。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
0
Help us understand the problem. What are the problem?