🐥
pressly/goose migrationをgoファイルで実行する
弊社では、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
これにより、マイグレーションの際に複数のクエリを実行できるようになります。
実装サンプル
下記のリポジトリにサンプルを用意しています。
採用情報
e-dashエンジニアチームは現在一緒にはたらく仲間を募集中です!
同じ夢について語り合える仲間と一緒に、環境問題を解決するプロダクトを作りませんか?
Discussion