🐥

pressly/goose migrationをgoファイルで実行する

2024/06/27に公開

弊社では、migrationにpressly/gooseを使用しており、sqlファイルに処理を記述して実行しています。

pressly/gooseでは、sqlファイルしか使用できないと思っていたのですが、golangでも実行できることを知ったので、試してみることにしました。

goファイルで実行するメリット、デメリット

メリット

  • 定数の利用: 定数を使用することでハードコードを避け、コードの管理が容易になります。
  • 構造体の利用: JSON型データを保存する際に、Golangの構造体を利用して記述できます。
  • 柔軟なロジック: Golangの関数を使って複雑なロジックや条件分岐を実装できるため、より柔軟なマイグレーションが可能です。

デメリット

  • 複雑なロジック: マイグレーションのロジックが複雑になると、コードのメンテナンスが困難になる可能性があります。
  • デバッグの難しさ: SQLファイルに比べ、データベース操作を直感的に理解するのが難しくなり、デバッグが困難になることがあります。

goファイルでmigrationを作成する

gooseをinstallする

gooseをinstallします。

go install github.com/pressly/goose/v3/cmd/goose@latest

migrationファイルを作成する

下記コマンドでmigrationファイルを作成します。

goose -dir ./path/to/migrations create create_users go

実行したいクエリを設定します。

package migrations

import (
	"context"
	"database/sql"

	"github.com/pressly/goose/v3"
	_ "gorm.io/driver/mysql"
)

func init() {
	goose.AddMigrationContext(upCreateUsers, downCreateUsers)
}

func upCreateUsers(ctx context.Context, tx *sql.Tx) error {
	// This code is executed when the migration is applied.
	query := `
	CREATE TABLE users (
		id INT AUTO_INCREMENT PRIMARY KEY,
		username VARCHAR(255) NOT NULL,
		email VARCHAR(255) NOT NULL,
		password VARCHAR(255) NOT NULL,
		profile_info JSON,
		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
	);
	`
	_, err := tx.ExecContext(ctx, query)
	if err != nil {
		return err
	}

	return nil
}

func downCreateUsers(ctx context.Context, tx *sql.Tx) error {
	// This code is executed when the migration is rolled back.
	query := `
	DROP TABLE IF EXISTS users;
	`
	_, err := tx.ExecContext(ctx, query)
	if err != nil {
		return err
	}

	return nil
}

migrationを実行する

こちらを参考にメインプログラムを作成します。
この時に、mainプログラムをmigrationsファイルと分離し、migrationsを格納したフォルダをimportしておくことで、ビルド処理を省略して直接実行することができます。

// This is custom goose binary with mysql support only.

package main

import (
	"flag"
	"log"
	"os"

	"github.com/pressly/goose/v3"
	_ "github.com/go-sql-driver/mysql"
	_ "github.com/goose-go-migrate-exasmple/infrastructure/db/migrations"
)

var (
	flags = flag.NewFlagSet("goose", flag.ExitOnError)
	dir   = flags.String("dir", ".", "directory with migration files")
)

func main() {
	flags.Parse(os.Args[1:]) // Parse first to handle the flags.
	args := flags.Args()     // Then retrieve the remaining arguments.

	if len(args) < 2 { // Adjusting for the actual minimum arguments required.
		flags.Usage()
		return
	}

	dbstring, command := args[0], args[1] // Adjusted indices according to the new understanding.

	db, err := goose.OpenDBWithDriver("mysql", dbstring)
	if err != nil {
		log.Fatalf("goose: failed to open DB: %v\\n", err)
	}
	defer func() {
		if err := db.Close(); err != nil {
			log.Fatalf("goose: failed to close DB: %v\\n", err)
		}
	}()

	arguments := []string{}
	if len(args) > 2 {
		arguments = append(arguments, args[2:]...)
	}

	if err := goose.RunContext(context.Background(), command, db, *dir, arguments...); err != nil {
		log.Fatalf("goose %v: %v", command, err)
	}
}

下記コマンドで実行します。

 go run main.go -dir ./path/to/migrations dbuser:dbpassword@tcp(127.0.0.1:3306)/dbname up

ExecContextで複数クエリを実行する

MySQLではセキュリティ上の理由から、デフォルトでは複数のクエリ実行が制限されています。

これは、ユーザー入力によるSQLインジェクション攻撃を防ぐためです。

例えば、「; DROP TABLE users;」のような悪意のある入力がエスケープ処理されずに実行されると、データベースが削除されるなどの脆弱性が発生する可能性があります。

しかし、マイグレーションを実行する場合には、信頼できるソースからのクエリのみが実行されるため、この制限を緩和しても問題ないと考えられます。

複数クエリの実行を許可するには、DB接続文字列に &multiStatements=true を追加します。

user:password@tcp(127.0.0.1:3306)/exampledb?parseTime=true&multiStatements=true

これにより、マイグレーションの際に複数のクエリを実行できるようになります。

実装サンプル

下記のリポジトリにサンプルを用意しています。

https://github.com/k-takeuchi220/goose-go-migrate-example

採用情報

e-dashエンジニアチームは現在一緒にはたらく仲間を募集中です!
同じ夢について語り合える仲間と一緒に、環境問題を解決するプロダクトを作りませんか?

Discussion