feature image

2025年12月14日 | ブログ記事

DBマイグレーションツールをgormigrateからAtlasに変えた

この記事は traP アドベントカレンダー2025 14 日目の記事です。

@ikura-hamu です。traP では主に Go でサーバーアプリを書いています。SysAd 班では部内メッセージングサービス traQ をはじめとしていくつかの部内サービスを開発・運用していますが、僕はゲーム管理・配布アプリである traP Collection というサービスのサーバーを書いています。

GitHub - traPtitech/trap-collection-server: 内製ゲーム管理システム traP Collection サーバーアプリケーション
内製ゲーム管理システム traP Collection サーバーアプリケーション. Contribute to traPtitech/trap-collection-server development by creating an account on GitHub.
traP Collection のサーバーアプリのリポジトリ

このプロジェクトでは、DB 操作のライブラリとして gorm を使っています。以前は DB (MariaDB)のマイグレーションに github.com/go-gormigrate/gormigrate (以下 gormigrate)を使っていましたが、現在は Atlas というツールを使っています。Atlas という名前がついているものは、MongoDB のクラウドのやつとか、OpenAI が出したブラウザとか、いろいろたくさんありますが、DB スキーマ管理ツールの Atlas です。

この記事は、gormigrate によるマイグレーションから Atlas によるマイグレーションへの移行を行った動機と手順を説明します。

Atlas
Atlas is a language-agnostic tool for managing and migrating database schemas using modern DevOps principles. It enables developers to automate schema changes through both declarative (schema-as-code) and versioned migration workflows, supporting inputs like HCL, SQL, and ORM models. Key features include automatic migration planning, schema diffs, linting, testing, and CI/CD for databases to ensure a reliable and scalable approach to database schema changes.
Atlas

gormigrate によるマイグレーションの課題

gormigrate によるマイグレーションは、規模が大きくなるにつれていくつかの課題が出てきます。
例があるとわかりやすいので、以下のような2段階のマイグレーションを考えます。

  1. users テーブル(idカラムとnameカラム)を作る
  2. users テーブルにemailカラムを追加する

また、traP Collectionの設計上の制約として、DB スキーマを表す構造体をマイグレーションのパッケージに public で配置することを設けます。

gormigrate では1段階目のマイグレーションを以下のように書けます。

package migrate

import (
  "fmt"
  "github.com/go-gormigrate/gormigrate/v2"
  "gorm.io/gorm"
)

// パッケージ外からアクセスできるようにする
type Users = usersV1

// スキーマの構造体
type usersV1 struct {
    id   int
    name string
}

// テーブル名の指定
func (userV1) TableName() string {
  return "users"
}

func Migrate(db *gorm.DB) error {
  migrations := []*gormigrate.Migration{
    {
      // マイグレーションの処理
      Migrate: func(tx *gorm.DB) error {
		return tx.Migrator().CreateTable(&userV1{})
	  },
      // ロールバックの処理
	  Rollback: func(tx *gorm.DB) error {
	    return tx.Migrator().DropTable("users")
      },
    },
  }
  
  m := gormigrate.New(db, gormigrate.DefaultOptions, migrations)
  if err := m.Migrate(); err != nil {
	return fmt.Errorf("Migration failed: %w", err)
  }
  
  return nil
}

Migrate 関数の中でマイグレーションを定義して実行していることが分かると思います。

次にマイグレーションの2ステップ目を書き足すと以下のようになります。

package migrate

import (
  "fmt"
  "github.com/go-gormigrate/gormigrate/v2"
  "gorm.io/gorm"
)

// パッケージ外からアクセスできるようにする
- type Users = usersV1
+ type Users = usersV2

// スキーマの構造体
type usersV1 struct {
  id   int
  name string
}

// テーブル名の指定
func (userV1) TableName() string {
  return "users"
}

+ // 新しいスキーマの構造体
+ type usersV2 struct {
+   id   int
+   name  string
+   email string
+ }
+
+ // テーブル名の指定
+ func (userV2) TableName() string {
+   return "users"
+ }

func Migrate(db *gorm.DB) error {
  migrations := []*gormigrate.Migration{
    {
      // マイグレーションの処理
      Migrate: func(tx *gorm.DB) error {
		return tx.Migrator().CreateTable(&userV1{})
	  },
      // ロールバックの処理
	  Rollback: func(tx *gorm.DB) error {
	    return tx.Migrator().DropTable("users")
      },
    },
+     {
+       // マイグレーションの処理
+       Migrate: func(tx *gorm.DB) error {
+ 		  return tx.Migrator().AddColumn(&userV2{}, "email")
+  	    },
+       // ロールバックの処理
+       Rollback: func(tx *gorm.DB) error {
+ 	      return tx.Migrator().DropTable("users")
+       },
+     },
  }
  
  m := gormigrate.New(db, gormigrate.DefaultOptions, migrations)
  if err := m.Migrate(); err != nil {
	return fmt.Errorf("Migration failed: %w", err)
  }
  
  return nil
}
Go のシンタックスハイライトついてるバージョン
package migrate

import (
  "fmt"
  "github.com/go-gormigrate/gormigrate/v2"
  "gorm.io/gorm"
)

// パッケージ外からアクセスできるようにする
type Users = usersV2

// スキーマの構造体
type usersV1 struct {
  id   int
  name string
}

// テーブル名の指定
func (userV1) TableName() string {
  return "users"
}

// 新しいスキーマの構造体
type usersV2 struct {
  id   int
  name  string
  email string
}

// テーブル名の指定
func (userV2) TableName() string {
  return "users"
}

func Migrate(db *gorm.DB) error {
  migrations := []*gormigrate.Migration{
    {
      // マイグレーションの処理
      Migrate: func(tx *gorm.DB) error {
		return tx.Migrator().CreateTable(&userV1{})
	  },
      // ロールバックの処理
	  Rollback: func(tx *gorm.DB) error {
	    return tx.Migrator().DropTable("users")
      },
    },
    {
      // マイグレーションの処理
      Migrate: func(tx *gorm.DB) error {
		return tx.Migrator().AddColumn(&userV2{}, "email")
	  },
      // ロールバックの処理
	  Rollback: func(tx *gorm.DB) error {
	    return tx.Migrator().DropTable("users")
      },
    },
  }
  
  m := gormigrate.New(db, gormigrate.DefaultOptions, migrations)
  if err := m.Migrate(); err != nil {
	return fmt.Errorf("Migration failed: %w", err)
  }
  
  return nil
}

このようなマイグレーションには以下のような課題があります。

これらの課題から、gormigrate は中・大規模なサービスの継続的な開発に向いていないことが分かると思います。実際、gormigrate のドキュメントでも、シンプルで最低限のツールとして作られたことが示されています。

Gormigrate was born to be a simple and minimalistic migration tool for small projects that uses Gorm.

https://github.com/go-gormigrate/gormigrate/blob/6c430d86d5bd92d4ffbb6a30cb9236f051d093ec/README.md

実際にはこの処理を複数のファイルに分割して記述することになりますが、課題が残ることに変わりはありません。
traP Collection では gormigrate を使ったマイグレーションを行っていましたが、これらの課題が大きくなってきたこと、今後の開発を進めるうえでマイグレーションが多く発生すると想定されることを踏まえて、異なるマイグレーション方法への移行を行いました。

移行前のマイグレーションコードはリポジトリに残っているため、確認することができます。

https://github.com/traPtitech/trap-collection-server/tree/bcef6df4e9b74633640666c63a3cf65523c3ccba/src/repository/gorm2/migrate

Atlas の特徴と採用した理由

Atlas は、様々な言語、ライブラリ、データベースに対応しているスキーマ管理ツールです。SQL や独自の HCL による宣言的なスキーマからマイグレーションコードを生成することができます。宣言的なスキーマと現在の DB の状態の差分を取り出してマイグレーションの SQL を生成します。マイグレーションの実行・ロールバックも行うことができます。traP Collection では採用していませんが、お金を払うことでクラウド上のサービスを含めてすべての機能を使うことができるようになるようです。

Atlas では宣言的なスキーマを書くわけですが、このスキーマには様々なフレームワーク・ORMのコードそのものを使うこともできます。Gorm も対応に含まれており、atlas-provider-gorm というツールを使うことで、 Gorm の構造体からマイグレーションのコードを生成することができます。実体としては、Gorm の構造体から理想的な状態のスキーマの SQL を生成し、それと現在のDBスキーマを比較することでマイグレーションコードを生成しているようです。

Atlas
Learn how to integrate Atlas with popular ORMs and frameworks like SQLAlchemy, GORM, Prisma, and Hibernate to automate and manage schema migrations. Explore language-specific guides for Python, Go, Node.js, and more.
https://atlasgo.io/orms

traP Collection で Atlas を採用した理由としては、以前の gormigrate による課題を宣言的なスキーマによって解決できると考えたからです。以前も理想のテーブルスキーマを構造体で記述していたので、宣言的と言うこともできるかもしれませんが、その構造体を使ってマイグレーションを実行するコードは自分で書く必要があり、これが負担になっていました。Atlas を使うことで、構造体を書き換えたらマイグレーションのコードは Atlas が勝手に作ってくれるため、開発時の負担を軽減することができます。さらに、テーブルスキーマを構造体で表す運用を変えずに済むため、移行が楽にできると考えられることも決定の要因となりました。

gormigrate から Atlas への移行手順

いくつかの手順を踏んで gormigrate から Atlas への移行を行いました。起動時にマイグレーションを実行するプログラムへの組み込みや設定ファイルの記述なども行っていますが、この記事ではテーブルスキーマ周りの処理について記述します。
以下の PR で移行を行っています。あまりコミットは整理されていませんが、興味がある方は見てみてください。

マイグレーションをAtlasに移行 by ikura-hamu · Pull Request #1216 · traPtitech/trap-collection-server
fix #1139マイグレーションに Atlas を使うように変更した 🩹 atlas のツール🔧 localhostからdbにアクセスできるようにする🏭 現在のスキーマを生成✨ atlasをアプリに組み込む✅ データ入れる系マイグレーションのテスト🩹 wireの設定🔧 ローカル環境の設定🩹 本番のイメージにatlas入れる🩹 データ入れる系のmigrationをSQLで…
https://github.com/traPtitech/trap-collection-server/pull/1216

移行前

移行前は、migrate というパッケージにテーブルスキーマを表す構造体が private の状態で配置されていました。この構造体には以前の例で出てきた usersV1usersV2 のように異なるマイグレーションのバージョンのスキーマが含まれています。この構造体のうち、最新のスキーマを表す構造体は型エイリアスを使って public にして他のパッケージからも使えるようにしていました。

現在のスキーマの生成

atlas migrate diff というコマンドを使うと、現在の DB の状態と Atlas が管理しているスキーマの差分を調べてマイグレーションのコードを生成してくれます。このコマンドを使って、gormigrate によって作られた現在のスキーマと同じスキーマを作るための SQL を生成します。

https://github.com/traPtitech/trap-collection-server/pull/1216/changes/ac553a75af5ad2c88739ddc7999d91c82bed3b83

マスタデータ系のテーブルの挿入のための SQL を書く

traP Collection のサーバーでは、例えば扱うことができる画像ファイルの種類(png, jpegなど)をマスタデータとして DB のテーブルに格納しています。gormigrate を使っていた際はレコードを挿入するような Go のコードを書いていましたが、Atlas はこれに対応する機能がありません。そのため、今回は Atlas が生成したマイグレーションの SQL ファイルに書き足す形でデータの挿入を行うようにしました。Atlas が生成したファイルを書き換える際は、atlas migrate hash コマンドを実行して、Atlas が管理するハッシュファイル(atlas.sum)を更新してあげる必要があります。

https://github.com/traPtitech/trap-collection-server/pull/1216/changes/3f59847e1de5b14a7d46d23c421d6951afa84b6c

スキーマの構造体の複製

新しいマイグレーション用のパッケージ schema を作ります。そこに migrate パッケージに記述されているスキーマのうち最新のものをコピーしてきます。ただし、コピーしてきた構造体は public になるよう記述し、V1 のような suffix はつけません。

https://github.com/traPtitech/trap-collection-server/pull/1216/changes/6af646129fdc05a22fa97eb2ec7aa476d74d866f

差分が少なくなるように構造体を修正する

前の段階で現在のスキーマを表す構造体をコピーしたはずです。新しくコピーした構造体を使って atlas migrate diff を実行してマイグレーションファイルを生成しても、差分が存在しないのが理想的ですが、実際には型エイリアスを使っていた影響でインデックスの名前などが意図しないものになっている場合などがあり、差分が出てしまいました。これに対応するために、できるだけ構造体のフィールド名やタグを書き換えて差分が少なくなるようにし、どうしても必要な分だけ新たにマイグレーションファイルを生成しました。

https://github.com/traPtitech/trap-collection-server/pull/1216/changes/aba5d64da345d0a95bfddhttps://github.com/traPtitech/trap-collection-server/pull/1216/changes/07abdec303c9c0666e0
d4cef247e2cfecc97e4b9502d36f8e76ba2193e9

migrate パッケージの型エイリアスが指す先を schema パッケージにする

元の migrate パッケージにあった public な型エイリアスが新しい schema パッケージにコピーした構造体になるように書き換えます。この書き換えを行うことによって、今まで外部で migrate を参照していた部分が自動的に schema パッケージを使えるようになります。

https://github.com/traPtitech/trap-collection-server/pull/1216/changes/9e2cfcca745cb133dd382469d24ec54ebd0f1b8b

ここまでで、アプリケーションコードを書き替えずにマイグレーション手法を gormigrate から Atlas に移行できました。

移行によって得られたもの

移行によって、テーブルに変更する際の処理が簡単になりました。

以前は以下のようなステップでした。

  1. 変更を加えたい構造体を suffix を変えてコピーする。
  2. 構造体に変更を加える。
  3. 外部から使われる型エイリアスが指す先を新しい構造体にする。(<- 忘れがち
  4. マイグレーションのためのプログラムを書く。(<- ミスりがち

Atlas の導入によって以下のようになりました。

  1. 構造体に変更を加える
  2. Atlas を使ってコードを生成する

とても簡単に、ミスもおきづらくなりました。マイグレーションをするのが難しいと新しい機能の追加への障壁が高くなってしまいます。マイグレーションが簡単になったので、どんどん新しい機能の開発を進めていきたいです。

明日は @tidus さんと @sumire0517 さんです。

ikura-hamu icon
この記事を書いた人
ikura-hamu

SysAd班、ゲーム班 いろいろやりたい

この記事をシェア

このエントリーをはてなブックマークに追加
共有

関連する記事

2021年8月12日
CPCTFを支えたWebshell
mazrean icon mazrean
2021年5月19日
CPCTF2021を実現させたスコアサーバー
xxpoxx icon xxpoxx
2025年12月13日
1-Monthon_25で学ぶ動画編集の小技集
hijoushiki icon hijoushiki
2024年6月21日
ハッカソン参加記 4班"Slide Center"
Alt--er icon Alt--er
2024年3月15日
個人開発として2週間でWebサービスを作ってみた話 〜「LABEL」の紹介〜
Natsuki icon Natsuki
2023年10月20日
DIGI-CON HACKATHON 参加記事「Comic DoQ」
mehm8128 icon mehm8128
記事一覧 タグ一覧 Google アナリティクスについて 特定商取引法に基づく表記