Bun を使ったmigration の実装例
はじめに
Go言語でORM を使う場合、gorm が有名ですが、最近はBun というORM も注目されています。
Bun は、migration の機能を持っていますが、生成されるmigrationファイルの内容は自分で書く必要があるため、TypeORMやDjangoと比較すると少し弱いと感じる面もあります。
Bun のリポジトリで実装例が紹介されているので、それを参考に今回はpostgresql を使用したmigration の実装例を紹介します。
詳細は公式のドキュメントを参照してください
Bun のmigration の実装例
migration を実行するためには2つのmain.go ファイルが必要になるため、この2つを作成します。
- migrate コマンドを実行するためのmain.go
- /migrations ディレクトリ内部でmigration ファイルを実行するためのmain.go
最終的に出来上がるディレクトリの構成は下記のようになります。
migrate
├── migrations
│ ├── {migrationコマンドで作成されたファイル}.go
│ ├── 20210831100001_create_books.go
│ └── main.go
└── main.go
migration コマンド実行用のmain.go を作成する
package main
import (
"database/sql"
"example/migrate/migrations"
"fmt"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/uptrace/bun/driver/pgdriver"
"github.com/uptrace/bun/extra/bundebug"
"log"
"os"
"strings"
"github.com/uptrace/bun/migrate"
"github.com/urfave/cli/v2"
"github.com/uptrace/bun"
)
func main() {
dsn := "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
// Open a database. Note that this is not an actual connection to the database.
// 今回はpostgresql を使用するため、pgdriver を使用しています。
// driver を間違えると、migration が正しく実行されないので注意してください。
openDB := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn)))
// Create a Bun instance. We will use it to perform queries.
db := bun.NewDB(openDB, pgdialect.New())
// Enable query logging.
db.AddQueryHook(bundebug.NewQueryHook(
bundebug.WithEnabled(false),
bundebug.FromEnv(""),
))
// Run migrations.
app := &cli.App{
Name: "bun",
// Pass the database to the sub-commands.
Commands: []*cli.Command{
newDBCommand(migrate.NewMigrator(db, migrations.Migrations)),
},
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
// newDBCommand returns a new CLI command for database migrations.
func newDBCommand(migrator *migrate.Migrator) *cli.Command {
return &cli.Command{
Name: "db",
Usage: "database migrations",
Subcommands: []*cli.Command{
{
Name: "init",
Usage: "create migration tables",
Action: func(c *cli.Context) error {
return migrator.Init(c.Context)
},
},
{
Name: "migrate",
Usage: "migrate database",
Action: func(c *cli.Context) error {
if err := migrator.Lock(c.Context); err != nil {
return err
}
defer migrator.Unlock(c.Context) //nolint:errcheck
group, err := migrator.Migrate(c.Context)
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no new migrations to run (database is up to date)\n")
return nil
}
fmt.Printf("migrated to %s\n", group)
return nil
},
},
{
Name: "rollback",
Usage: "rollback the last migration group",
Action: func(c *cli.Context) error {
if err := migrator.Lock(c.Context); err != nil {
return err
}
defer migrator.Unlock(c.Context) //nolint:errcheck
group, err := migrator.Rollback(c.Context)
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no groups to roll back\n")
return nil
}
fmt.Printf("rolled back %s\n", group)
return nil
},
},
{
Name: "lock",
Usage: "lock migrations",
Action: func(c *cli.Context) error {
return migrator.Lock(c.Context)
},
},
{
Name: "unlock",
Usage: "unlock migrations",
Action: func(c *cli.Context) error {
return migrator.Unlock(c.Context)
},
},
{
Name: "create_go",
Usage: "create Go migration",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
mf, err := migrator.CreateGoMigration(c.Context, name)
if err != nil {
return err
}
fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
return nil
},
},
{
Name: "create_sql",
Usage: "create up and down SQL migrations",
Action: func(c *cli.Context) error {
name := strings.Join(c.Args().Slice(), "_")
files, err := migrator.CreateSQLMigrations(c.Context, name)
if err != nil {
return err
}
for _, mf := range files {
fmt.Printf("created migration %s (%s)\n", mf.Name, mf.Path)
}
return nil
},
},
{
Name: "status",
Usage: "print migrations status",
Action: func(c *cli.Context) error {
ms, err := migrator.MigrationsWithStatus(c.Context)
if err != nil {
return err
}
fmt.Printf("migrations: %s\n", ms)
fmt.Printf("unapplied migrations: %s\n", ms.Unapplied())
fmt.Printf("last migration group: %s\n", ms.LastGroup())
return nil
},
},
{
Name: "mark_applied",
Usage: "mark migrations as applied without actually running them",
Action: func(c *cli.Context) error {
group, err := migrator.Migrate(c.Context, migrate.WithNopMigration())
if err != nil {
return err
}
if group.IsZero() {
fmt.Printf("there are no new migrations to mark as applied\n")
return nil
}
fmt.Printf("marked as applied %s\n", group)
return nil
},
},
},
}
}
/migrations ディレクトリ内部でmigration ファイルを実行するためのmain.go を作成する
package migrations
import "github.com/uptrace/bun/migrate"
var Migrations = migrate.NewMigrations()
func init() {
if err := Migrations.DiscoverCaller(); err != nil {
panic(err)
}
}
動作確認方法
必要なファイルを作成したら、実際にコマンドが実行できるかどうかを確認します。
bun はdb に対してmigration の順番を管理するために、bun_migrations
, bun_migration_locks
というテーブルを作成します。
init
コマンド を使用してテーブルを作成することで、コマンドが実行できるか?DB に対してSQL を実行できるか?を確認します。
$ go run {/migrations 配下のmain.go までのpath} db init
実行が完了した場合、下記のように bun_migrations
, bun_migration_locks
テーブルが作成されます。
migration ファイルの作成
コマンドの実行確認ができたら、次はmigration ファイルを使用したテーブルの作成を行います。
create_go
コマンドを使用して、migration ファイルを作成します。
生成されるファイルは20230923093308_answer.go
のように、{日付}_{名前}.go
の形式で作成されます。
$ go run {/migrations 配下のmain.go までのpath} db create_go {migrationファイルの名前}
作成されたmigration ファイルは下記のようにup
とdown
用の関数を持った状態で、テンプレートが作成されます。
実際にテーブルに対して行う操作は、開発者が自分で実装する必要があります。
discussion でも自分で書き込むこと。として返答されています(https://github.com/uptrace/bun/discussions/433)
package migrations
import (
"context"
"fmt"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
fmt.Print(" [up migration] ")
return nil
}, func(ctx context.Context, db *bun.DB) error {
fmt.Print(" [down migration] ")
return nil
})
}
テーブルを表現する構造体を定義する
migrate コマンドを実行する前に、作成するテーブル情報を持つ構造体を定義しましょう。
Bun は構造体のフィールドに付与されたタグをみて、テーブルの情報を作成します。
それぞれのフィールドに対して付与するタグは、下記のドキュメントに記載されているので、適宜参照してください。
model 作成ドキュメント
package model
import "github.com/uptrace/bun"
type User struct {
bun.BaseModel `bun:"table:users,alias:u"` // テーブル名はusers, エイリアスはu
ID string `bun:"id,pk,type:uuid"` // id はpk でuuid 型
Email string `bun:"type:varchar(255)"` // email はvarchar(255) 型
Password string `bun:"type:varchar(255)"` // password はvarchar(255) 型
}
migration ファイルにテーブルを作成する処理を実装する
User 構造体を参照し、user テーブルを作成するための処理を書いていきます。
up
ではテーブルを作成し、down
ではテーブルを削除する処理を実装します。
package migrations
import (
"context"
"example/repository/model"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error {
_, err := db.NewCreateTable().
// model.User は自分で定義したUser 構造体
Model((*model.User)(nil)).
Exec(ctx)
if err != nil {
return err
}
return nil
}, func(ctx context.Context, db *bun.DB) error {
_, err := db.NewDropTable().
Model((*model.User)(nil)).
IfExists().
Exec(ctx)
if err != nil {
return err
}
return nil
})
}
migrate コマンドの実行
$ go run {/migrations 配下のmain.go までのpath} db migrate
実行が完了した場合、下記のようにbun_migrations
テーブルにmigration の情報が保存され、User テーブルが作成されます。
おわりに
今回は、Bun を使用したmigration の実装例を紹介しました。
Bun はmigration だけでなく、基本的な使用方法などもexample として紹介されているので、興味がある方はぜひ参考にしてみてください。
Bun の実装例は下記のリポジトリで確認できます。
Discussion