Open6

goのORMのentとデータベースのschema管理ツールatlasでVersioned Migrationする

renoinnrenoinn

sampleappディレクトリにアプリケーションを作成していく。データベース関連はdatasourceディレクトリにまとめることにした。

mkdir -p ~/go/src/github.com/renoinn/sampleapp
cd ~/go/src/github.com/renoinn/sampleapp
go mod init
mkdir datasource
cd datasource
go run -mod=mod entgo.io/ent/cmd/ent init Users --target ./datasource/ent/schema

こんな感じでファイルが出力される。

datasource/ent/schema/user.go
package schema

import "entgo.io/ent"

// User holds the schema definition for the Users entity.
type Users struct {
    ent.Schema
}

// Fields of the Users.
func (Users) Fields() []ent.Field {
    return []ent.Field {
        field.String("name"),
        field.String("email").Unique()
    }
}

// Edges of the Users.
func (Users) Edges() []ent.Edge {
    return nil
}

フィールドの部分にカラムを追加してgo generateする

datasource/ent/schema/user.go
...

// Fields of the Users.
func (Users) Fields() []ent.Field {
    return []ent.Field {
        field.String("name"),
        field.String("email").Unique()
    }
}

...

テーブル名と型名が同名じゃない場合はアノテーションで設定する。

// User Annotations
func (User) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "Users"},
    }
}
go generate ./datasource/ent

./ent以下に自動生成されたファイルがたくさんできるはず。
あとは接続する処理を書く。

cmd/migration/main.go
package main

import (
    "context"
    "log"

    "github.com/renoinn/sampleapp/datasource/ent"

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

func main() {
    client, err := ent.Open("mysql", "<user>:<pass>@tcp(<host>:<port>)/<database>?parseTime=True")
    if err != nil {
        log.Fatalf("failed opening connection to mysql: %v", err)
    }
    defer client.Close()
    // Run the auto migration tool.
    if err := client.Schema.Create(context.Background()); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }
}

このmain.goを起動したらマイグレーションされる。

renoinnrenoinn

ここからatlasで変更管理をしていく。

今回はUbuntuなので、以下のコマンドでatlasをインストールする。

curl -LO https://release.ariga.io/atlas/atlas-linux-amd64-latest
sudo install -o root -g root -m 0755 ./atlas-linux-amd64-latest /usr/local/bin/atlas

こんな感じのコマンドでschemaをhcl形式で吐き出してくれたりする。

atlas schema inspect -u "mysql://<user>:<password>@localhost:3306/<dbname>" > schema.hcl

generate.goに--feature sql/versioned-migrationのオプションを追加してgo generateする。

datasource/ent/generate.go
 package ent

//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate --feature sql/versioned-migration ./schema
go generate ./datasource/ent

すると、datasource/ent/migrateに差分を取るためのメソッドが追加されるので、それを使うようにする。

cmd/migration/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

    atlas "ariga.io/atlas/sql/migrate"
	"entgo.io/ent/dialect"
	"entgo.io/ent/dialect/sql/schema"
	_ "github.com/go-sql-driver/mysql"
	"github.com/renoinn/sampleapp/datasource/ent/migrate"
)

func main() {
    dataSourceName := fmt.Sprintf("mysql://%s:%s@%s/%s?charset=utf8&parseTime=True", "sample_user", "sample_password", "localhost:3306", "sample_db")
    ctx := context.Background()

    // Create a local migration directory able to understand Atlas migration file format for replay.
    dir, err := atlas.NewLocalDir("datasource/ent/migrate/migrations")
    if err != nil {
        log.Fatalf("failed creating atlas migration directory: %v", err)
    }

    // Migrate diff options.
    opts := []schema.MigrateOption{
        schema.WithDir(dir),                         // provide migration directory
        schema.WithMigrationMode(schema.ModeReplay), // provide migration mode
        schema.WithDialect(dialect.MySQL),           // Ent dialect to use
        schema.WithFormatter(atlas.DefaultFormatter),
    }

    // Generate migrations using Atlas support for MySQL (note the Ent dialect option passed above).
    err = migrate.NamedDiff(ctx, dataSourceName, os.Args[1], opts...)
    if err != nil {
        log.Fatalf("failed generating migration file: %v", err)
    }
}

こんな感じに書き換えて実行する。datasource/ent/migrate/migrationsディレクトリは事前に作っておく。

go run -mod=mod ./cmd/migration/main.go create_users
  • datasource/ent/migrate/migrations/20221104140216_create_users.sql
  • datasource/ent/migrate/migrations/atlas.sum

の2ファイルが生成される。

20221104140216_create_users.sql
-- create "users" table
CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;

Ent的には生成されたSQLをAtlasのCLIで実行するのがオススメらしい。

atlas migrate apply \
  --dir "file://datasource/ent/migrate/migrations"
  --url mysql://sample_user:sample_password@localhost:3306/sample_db

Migrating to version 20221104140216 (1 migrations in total):

  -- migrating version 20221104140216
    -> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
  -- ok (75.061732ms)

  -------------------------
  -- 88.741096ms
  -- 1 migrations
  -- 1 sql statements

atlas.sumはマイグレーションの競合を検出しやすくするためのファイルらしい。
2つのチームが別々のブランチでマイグレーションを更新している場合、先にマージした方のatlas.sumに対して、後からマージする側がコンフリクトすることになるので、この段階で両チームがマイグレーションに変更を加えようとしてることを検出できると。

ent + atlasで生成されたファイルに後から手動で変更を加えていないかどうかは下記のコマンドでチェックできる。

atlas migrate validate --dir file://<path-to-your-migration-directory>

https://entgo.io/docs/versioned-migrations#atlas-migration-directory-integrity-file
https://github.com/ent/ent/tree/master/examples/migration

renoinnrenoinn

マイグレーションを追加する。

cmd/migration/main.goのMigrateOptionでMigrationModeをModeInspectに修正する。

cmd/migration/main.go
opts := []schema.MigrateOption{
    schema.WithDir(dir),                         // provide migration directory
    schema.WithMigrationMode(schema.ModeInspect), // provide migration mode
    schema.WithDialect(dialect.MySQL),           // Ent dialect to use
    schema.WithFormatter(atlas.DefaultFormatter),
}
go run -mod=mod entgo.io/ent/cmd/ent init Site --target ./datasource/ent/schema
(./datasource/ent/schema/site.goを編集)
go generate ./datasource/ent
go run -mod=mod ./cmd/migration/main.go create_site
renoinnrenoinn

一度マイグレーション用のSQLファイルを生成した後に、フィールドなどを書き換える

go run -mod=mod ./cmd/migration/main.go create_site

するとdatasource/ent/migrate/migrationsに20221108025948_create_site.sqlが追加されて、atlas.sumが更新される。
で、この時に生成されたSQLを見て、「やっぱりこうしよう」みたいな感じで./datasource/ent/schema/site.goを修正して、再度

go generate ./datasource/ent
go run -mod=mod ./cmd/migration/main.go create_site

を実行すると、20221108025948_create_site.sqlが変更されるのではなく、datasource/ent/migrate/migrationsに20221108030142_create_site.sqlが追加される。

atlas migrate apply  --dir "file://datasource/ent/migrate/migrations"  --url mysql://user:password@localhost:3306/sample_db --dry-run
Migrating to version 20221108030142 (2 migrations in total):

  -- migrating version 20221108025948
    -> CREATE TABLE `sites` (`id` bigint NOT NULL AUTO_INCREMENT, `url` varchar(2048) NOT NULL, `title` varchar(100) NOT NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
    -> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(255) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
  -- ok (1.836131ms)

  -- migrating version 20221108030142
    -> CREATE TABLE `sites` (`id` bigint NOT NULL AUTO_INCREMENT, `url` varchar(2048) NOT NULL, `title` varchar(100) NOT NULL, PRIMARY KEY (`id`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
    -> CREATE TABLE `users` (`id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `email` varchar(100) NOT NULL, PRIMARY KEY (`id`), UNIQUE INDEX `email` (`email`)) CHARSET utf8mb4 COLLATE utf8mb4_bin;
  -- ok (1.887923ms)

atlas.sumの中身はこんな感じになる。

h1:ot0oyFCmaDgVv5CexXOAnjDBlxjBWm6ypxeBtryxswY=
20221108025948_create_site.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=
20221108030147_create_site.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=

この状態でapplyしようとしてもおかしなことになるので、どうにかする。
まず、不要な方のSQLファイルを削除する。

rm -rf datasource/ent/migrate/migrationsに20221108025948_create_site.sql

削除したら、atlas.sumを編集して削除した方のSQLファイルの行を削除する。

h1:ot0oyFCmaDgVv5CexXOAnjDBlxjBWm6ypxeBtryxswY=
- 20221108025948_create_site.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ= この行を削除
20221108030147_create_site.sql h1:P3lh3alKiSWJyatvaBPlRsv108Nj3gdzNOtB13Dn4HQ=

削除したらhashを生成し直す。

atlas migrate hash --dir "file://datasource/ent/migrate/migrations"

改めてdry-runして確認して問題なければapplyできる。

renoinnrenoinn

DB作成のdocker。

version: "3"

services:
  mysql:
    image: mysql:8.0
    container_name: mysql
    command: mysqld --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_DATABASE: db_name
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      MYSQL_ROOT_PASSWORD: root
  app:
    container_name: "app"
    image: cosmtrek/air
    working_dir: /var/app
    volumes:
      - ../.:/var/app
    tty: true
    ports:
      - "8080:8080"
 docker compose -f build/docker-compose.yml up -d --build