はじめに
データベースマイグレーションは、チーム開発において常に課題となりますね。スキーマの変更履歴管理、競合解決、ロールバック戦略など、考慮すべき点は多いです。本記事では、Atlasを使ったマイグレーション管理の実践例をご紹介します。
対象者
この記事は下記のような人を対象にしています。
- データベースマイグレーションの管理に課題を感じている方
- Drizzle ORMを使用していて、マイグレーションツールを検討している方
- チーム開発でのスキーマ変更の競合解決に困っている方
- TypeScriptでの型安全なスキーマ管理に興味がある方
Atlasとは
Atlasは、データベーススキーマのマイグレーションを管理するCLIツールです。特徴は以下の通りです:
- バージョン管理型マイグレーション(Versioned Migration)
- スキーマ定義からの自動差分生成
- Migration Directory Integrity(マイグレーションファイルの改ざん検知)
- チーム開発向けの競合解決機能
- 複数のORMとの連携(Drizzle、Prisma、TypeORMなど)
本記事では、以下の構成でAtlasを使用します:
- バージョン管理型マイグレーション(Versioned Migration)
- Drizzle ORMでTypeScriptによるスキーマ定義
external_schemaによる動的スキーマ読み込み
1. Drizzle ORMとの連携
external_schemaによる統合
Atlasの最大の利点は、ORMのスキーマ定義を直接読み込めることですね。atlas.hclの設定例を見てみましょう:
data "external_schema" "drizzle" {
program = ["mise", "run", "--raw", "dbSchemaExport"]
}
locals {
src = data.external_schema.drizzle.url
migration_dir = "file://db/migrations"
exclude_tables = [
"atlas_schema_revisions",
"__drizzle_migrations"
]
}
dbSchemaExportはdrizzle-kit exportを実行し、TypeScriptで定義されたスキーマをDDL形式でAtlasに渡します。これにより、以下のワークフローが実現できます:
- Drizzle ORMでスキーマをTypeScriptで定義
- Atlasが差分を自動計算
- マイグレーションファイルを生成
TypeScript-firstのスキーマ定義
Drizzleでスキーマを定義する例をご紹介します:
import { mysqlTable, varchar, timestamp } from 'drizzle-orm/mysql-core';
export const users = mysqlTable('m_users', {
id: varchar('id', { length: 36 }).primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }).notNull(),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow(),
});
このスキーマを基に、Atlasでマイグレーションを生成します:
# 差分を計算してマイグレーション生成
atlas migrate diff create_users \
--env local
# 生成されたマイグレーションファイル
# db/migrations/20260122120000_create_users.sql
スキーマ定義とマイグレーション生成を分離することで、型安全性とバージョン管理の両立が可能になります。
2. Dev Databaseによる差分計算
Atlasは、マイグレーション差分を計算するために「Dev Database」という一時的なDBを使用します。
セットアップスクリプト
atlas.hclでの設定を見てみましょう:
data "external" "setup_dev_db" {
program = ["./script/atlas/setupAtlasDevDb.sh", atlas.env]
}
env {
name = atlas.env
url = getenv("DATABASE_URI")
dev = trimspace(data.external.setup_dev_db)
src = local.src
migration {
dir = local.migration_dir
}
exclude = local.exclude_tables
}
setupAtlasDevDb.shの処理は以下のようになっています:
#!/bin/bash
ENV=$1
DEV_DB_NAME="atlasDev_${ENV}_$(date +%s)"
# Docker Compose内のMySQLに一時DB作成
docker compose exec -T db mysql -u root -p"${MYSQL_ROOT_PASSWORD}" <<SQL
CREATE DATABASE IF NOT EXISTS ${DEV_DB_NAME};
SQL
echo "mysql://root:${MYSQL_ROOT_PASSWORD}@localhost:3306/${DEV_DB_NAME}"
この仕組みにより:
- マイグレーション実行前に一時DBを作成
- 一時DBに現在のスキーマを適用
- 目標スキーマ(Drizzle定義)との差分を計算
- 差分をマイグレーションファイルとして出力
実際のDBに影響を与えずに、安全に差分計算ができますね。
3. マイグレーション操作の実践
基本的なマイグレーションフロー
スキーマ変更から適用までの完全なフローをお見せします。
1. TypeScriptスキーマを変更
// src/repository/db/schema/users.ts
export const users = mysqlTable('m_users', {
id: varchar('id', { length: 36 }).primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 100 }).notNull(),
// 新しいカラムを追加
role: varchar('role', { length: 20 }).notNull().default('member'),
createdAt: timestamp('created_at').defaultNow(),
});
2. マイグレーションファイル生成
atlas migrate diff add_user_role \
--env local
# 生成されたファイル: db/migrations/20260122120000_add_user_role.sql
3. 生成されたSQLを確認
cat db/migrations/20260122120000_add_user_role.sql
-- add_user_role.sql
ALTER TABLE `m_users` ADD COLUMN `role` VARCHAR(20) NOT NULL DEFAULT 'member';
意図した変更になっているか確認しましょう。必要に応じて手動編集も可能です。
4. (手動編集した場合)チェックサム再計算
マイグレーションファイルを手動編集した場合、ハッシュを再計算します:
atlas migrate hash \
--dir "file://db/migrations"
# atlas.sum が更新される
5. 整合性検証
atlas migrate validate \
--env local
# チェック内容:
# - atlas.sum のハッシュ一致
# - マイグレーションファイルの順序
# - SQL構文の妥当性
6. ドライラン(プレビュー)
atlas migrate apply \
--env local \
--dry-run
# 出力例:
# Migrating to version 20260122120000 (1 migrations in total):
#
# -- applying version 20260122120000
# -> ALTER TABLE `m_users` ADD COLUMN `role` VARCHAR(20) NOT NULL DEFAULT 'member';
#
# -- ok (2.5ms)
実行されるSQLを事前に確認できますね。
7. マイグレーション適用
atlas migrate apply \
--env local
# DBスキーマが更新される
# atlas_schema_revisions テーブルに履歴が記録される
カスタムマイグレーションの作成
自動生成では対応できない複雑な変更(データ移行、ストアドプロシージャなど)には、空のマイグレーションファイルを作成して手動編集します:
# 空のマイグレーションファイル作成
atlas migrate new migrate_user_data \
--env local
# 生成されたファイルを編集
# db/migrations/20260122130000_migrate_user_data.sql
-- migrate_user_data.sql
-- 既存ユーザーにデフォルトロールを付与
UPDATE m_users
SET role = 'member'
WHERE role IS NULL;
-- 制約を追加
ALTER TABLE m_users
MODIFY COLUMN role VARCHAR(20) NOT NULL;
ロールバック戦略
Atlasはロールバック用のSQLも自動生成します:
# 最新のマイグレーションを1つ戻す
atlas migrate down \
--env local
# 特定のバージョンまで戻す
atlas migrate down \
--env local \
--to-version 20260122120000
# プレビュー
atlas migrate down \
--env local \
--dry-run
実行順序の制御も可能です:
# 線形実行(デフォルト): 未適用のマイグレーションを順番に実行
atlas migrate apply --exec-order linear
# スキップ実行: 一部未適用でも最新まで適用
atlas migrate apply --exec-order linear-skip
# 非線形実行: 順序関係なく未適用分を実行
atlas migrate apply --exec-order non-linear
マイグレーション状況の確認
現在のマイグレーション適用状況はmigrate statusで確認できます:
atlas migrate status \
--env local
出力例:
Migration Status: PENDING
-- Current Version: 20260120024726
-- Next Version: 20260122053659
-- Pending: 1
Version Description Status Applied
20260115023702 init OK 2026-01-15
20260120024726 slack-message-json OK 2026-01-20
20260122053659 add-user-role Pending -
この出力から以下がわかります:
- Current Version: 現在適用されている最新のマイグレーション
- Next Version: 次に適用されるマイグレーション
- Pending: 未適用のマイグレーション数
- Status: 各マイグレーションの状態(OK=適用済み、Pending=未適用)
- Applied: 適用日時
4. チーム開発での運用
Migration Directory Integrity(atlas.sum)
atlas.sumファイルは、各マイグレーションファイルのハッシュ値を記録します:
h1:LDkRlVBEh74gnEmwRezxnzyhv3wRcuztl1B2HVzo1D0=
20260115023702_init.sql h1:4yKR5kDDqgdh1cZzhuAMsxKNlthWA5mSPtlYa38s5x4=
20260122120000_create_users.sql h1:8xK2pLmVHzn3mDsyQaA4pQcH1nXy7bR9w5D3cT1vE2A=
この仕組みにより:
- マイグレーションファイルの改ざん検知
- チーム開発での競合検出
- CI/CDでの整合性検証
が可能になります。
マイグレーション競合の解決
複数の開発者が同時にマイグレーションを作成すると、タイムスタンプが競合する可能性があります。
シナリオ:
develop ───────────────────────────────────────►
│ │
▼ ▼
Team A: 20260122_add_user_table マージ済み
Team B: 20260122_add_order_table これからマージ(競合)
- 開発者Aがブランチで
20260122120000_add_user_table.sqlを作成 - 開発者Bがブランチで
20260122120000_add_order_table.sqlを作成 - 開発者Aが先にdevelopにマージ
- 開発者Bがdevelopをマージすると、同じタイムスタンプのマイグレーションが存在
解決フロー:
1. developをマージ後、ステータス確認
git merge develop
atlas migrate status \
--env local
# 出力例:
# Migration Status: [OUT OF ORDER]
# Error: migration 20260122120000_add_order_table is out of order
[OUT OF ORDER]は、マイグレーションの順序が不正であることを示します。
2. 競合するマイグレーションをリベース
atlas migrate rebase 20260122120000 \
--env local
# 新しいタイムスタンプで再生成される:
# 20260122120000_add_order_table.sql → 20260123120000_add_order_table.sql
# atlas.sum も自動更新される
migrate rebaseは、指定したバージョンのマイグレーションに新しいタイムスタンプを付与します。
3. 整合性確認
atlas migrate validate \
--env local
# 出力:
# All migration files are valid
4. 適用
atlas migrate apply \
--env local
# 新しいタイムスタンプのマイグレーションが適用される
この仕組みにより、Git上でのファイル競合を避けつつ、マイグレーションの順序を保つことができます。
手動編集後のハッシュ更新
マイグレーションファイルを手動編集した場合、atlas.sumのハッシュが不整合になります:
# ハッシュを再計算
atlas migrate hash \
--env local
# atlas.sum が更新される
既存DBへの導入(ベースライン設定)
既にデータが存在するDBにAtlasを導入する場合、現在の状態をベースラインとして設定できます:
# 現在のDB状態をマイグレーション済みとマーク
atlas migrate set 20260122120000 \
--env production
# これ以降のマイグレーションのみ適用される
atlas migrate apply \
--env production
これにより、既存のテーブルを削除せずにAtlasを導入できますね。
5. Lint: 命名規則の強制
Atlasは、スキーマの命名規則をコード化し、自動チェックできます。
命名規則の定義
atlas.hclでの設定をご紹介します:
lint {
git {
base = "develop"
}
naming {
# テーブル名: プレフィックス(i_, m_, t_)+ スネークケース
table {
match = "^[mit]_[a-z][a-z0-9]*(_[a-z0-9]+)*$"
}
# カラム名: スネークケース
column {
match = "^[a-z][a-z0-9]*(_[a-z0-9]+)*$"
}
# インデックス: カラム名_idx
index {
match = "^[a-z][a-z0-9]*(_[a-z0-9]+)+_idx$"
}
# 外部キー: カラム名_fk
foreign_key {
match = "^[a-z][a-z0-9]*(_[a-z0-9]+)+_fk$"
}
}
}
この例では、テーブル名にi_、m_、t_といったプレフィックスを付ける規則を定義しています。プロジェクトの要件に応じて、独自の命名規則を強制できますね。
検証の実行
# developブランチとの差分をlint
atlas migrate lint \
--env local \
--git-base develop
# 出力例
# Error: naming violation: table "Users" does not match pattern "^[mit]_[a-z][a-z0-9]*(_[a-z0-9]+)*$"
# Suggestion: rename to "m_users"
CI/CDに組み込むことで、命名規則違反を自動検出できます:
# .github/workflows/atlas-lint.yml
name: Atlas Lint
on: [pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run Atlas Lint
run: atlas migrate lint --env ci --git-base origin/develop
6. その他の便利機能
Mermaid形式でのER図出力
format {
schema {
inspect = "{{ mermaid . }}"
}
migrate {
diff = "{{ sql . \" \" }}"
}
}
# スキーマをMermaid形式で出力
atlas schema inspect \
--env local \
--url "mysql://root:password@localhost:3306/mydb"
# 出力例
# erDiagram
# m_users {
# varchar id PK
# varchar email UK
# varchar name
# timestamp created_at
# }
# t_orders {
# varchar id PK
# varchar user_id FK
# decimal amount
# }
# m_users ||--o{ t_orders : "has"
この出力をドキュメントに埋め込むことで、ER図を自動生成できます。
マイグレーションの検証
# マイグレーションファイルの整合性検証
atlas migrate validate \
--env local
# チェック内容:
# - atlas.sum のハッシュ一致
# - マイグレーションファイルの順序
# - SQL構文の妥当性
CI/CDで実行することで、不正なマイグレーションのマージを防げます。
おわりに
本記事では、Atlasを使ったマイグレーション管理のポイントについてまとめました。
Drizzle連携
- TypeScriptでスキーマを定義し、型安全性を確保
external_schemaで自動差分生成- ORMとマイグレーションツールの責務分離
Dev Database
- 一時DBで安全に差分計算
- 本番DBに影響を与えない
- Docker Composeとの統合が容易
マイグレーション操作
migrate diff:差分自動生成migrate apply:適用(dry-runでプレビュー)migrate down:ロールバックmigrate new:カスタムマイグレーション
チーム開発
migrate rebase:タイムスタンプ競合解決migrate hash:手動編集後のハッシュ更新migrate set:既存DB移行のベースラインatlas.sum:改ざん検知とバージョン管理
品質管理
- Lint:命名規則の強制
- Validate:CI/CDでの自動検証
- Mermaid:ER図の自動生成
Atlasは、単なるマイグレーションツールではなく、スキーマ変更の履歴管理、チーム開発の調整、品質保証までをカバーする統合ツールです。Drizzle ORMと組み合わせることで、TypeScriptの型安全性を保ちながら、データベーススキーマを厳密に管理できます。
これらの機能を適切に使えば、データベースマイグレーションに関する多くの問題を解決できます。特に、複数人での開発や、複数環境(開発/ステージング/本番)での運用において、その真価を発揮しますね。
この記事がどなたかの参考になれば幸いです。