Tags: Go Golang

rubenv/sql-migrate でマイグレーションツールを実装

rubenv/sql-migrate でマイグレーションツールを実装する。

環境

  • go 1.19
  • github.com/rubenv/sql-migrate v1.2.0
  • MySQL 8.0

実装

ディレクトリ構成

root
|
|-cmd
| |-migration
|   |-migration.go
|
|-internal
  |-migration
    |-migration.go
    |-migrations
      |-0001.sql
      |-0002.sql
      |-...

マイグレーションファイルは internal/migration/migrations ディレクトリに SQL ファイルの形で配置する。

internal/migration/migration.go

標準パッケージである go:embed を使ってマイグレーションファイルを読み込む。
読み込み方法はどれを選べばいいのかわからず issue を探したところ AssetMigrationSource を使うのが正解らしい。

package migration

import (
	"database/sql"
	"embed"
	"fmt"

	"github.com/rubenv/sql-migrate"
)

//go:embed migrations/*.sql
var static embed.FS

type Migration struct {
	db *sql.DB
}

func NewMigration(db *sql.DB) *Migration {
	return &Migration{db}
}

func (m *Migration) MigrateUp() error {
	migrations := &migrate.AssetMigrationSource{
		Asset: static.ReadFile,
		AssetDir: func() func(string) ([]string, error) {
			return func(path string) ([]string, error) {
				return assetDir(path)
			}
		}(),
		Dir: "migrations",
	}

	n, err := migrate.Exec(m.db, "mysql", migrations, migrate.Up)
	fmt.Printf("Applied %d migrations!\n", n)
	return err
}

func assetDir(path string) ([]string, error) {
	dirEntry, err := static.ReadDir(path)
	if err != nil {
		return nil, err
	}
	entries := make([]string, 0)
	for _, e := range dirEntry {
		entries = append(entries, e.Name())
	}

	return entries, nil
}

MigrateDown は今のところ呼ぶ予定が無いので実装していない。

マイグレーションファイル

rubenv/sql-migrate の README に書いてある通りだが、基本的にはこんな感じの SQL を書いて sql ファイルとして保存する。

-- +migrate Up
create table config (
    id int primary key comment "設定ID",
    allow_register tinyint not null comment "新規登録可能フラグ"
) comment = "設定";

-- +migrate Down
drop table config;

これを internal/migration/migrations に入れておき、前述の go:embed を使ってコンパイル時にバイナリに埋め込む。
バイナリに埋め込まれるので実行時にファイルを別途配置する必要は無くて楽。

あと個人的に SQL は生の SQL として書いてあるほうが理解しやすいので、生の SQL で書けるのが助かる。

cmd/migration/migration.go

DB に接続して MigrateUp を呼ぶ。

package main

import (
	"database/sql"
	"os"

	_ "github.com/go-sql-driver/mysql"

	"gitlab.com/k1350/sololog_gql/internal/migration"
)

func main() {
	dsn := os.Getenv("DSN")

	db, err := sql.Open("mysql", dsn)
	if err != nil {
        writeLog("EMERGENCY", err)
	}
	defer db.Close()
	if err = db.Ping(); err != nil {
		writeLog("EMERGENCY", err)
	}

	m := migration.NewMigration(db)
	err = m.MigrateUp()
	if err != nil {
		writeLog("EMERGENCY", err)
	}
}

DSN は環境変数で DSN=db_user:db_pass@tcp(db)/sololog?parseTime=true という形式で指定する。
parseTime=true は必須。

以上