Atlas + Drizzle ORMで実現するType-SafeなDBマイグレーション戦略

Atlas + Drizzle ORMで実現するType-SafeなDBマイグレーション戦略

はじめに

データベースマイグレーションは、チーム開発において常に課題となりますね。スキーマの変更履歴管理、競合解決、ロールバック戦略など、考慮すべき点は多いです。本記事では、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"
  ]
}

dbSchemaExportdrizzle-kit exportを実行し、TypeScriptで定義されたスキーマをDDL形式でAtlasに渡します。これにより、以下のワークフローが実現できます:

  1. Drizzle ORMでスキーマをTypeScriptで定義
  2. Atlasが差分を自動計算
  3. マイグレーションファイルを生成

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}"

この仕組みにより:

  1. マイグレーション実行前に一時DBを作成
  2. 一時DBに現在のスキーマを適用
  3. 目標スキーマ(Drizzle定義)との差分を計算
  4. 差分をマイグレーションファイルとして出力

実際の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  これからマージ(競合)
  1. 開発者Aがブランチで20260122120000_add_user_table.sqlを作成
  2. 開発者Bがブランチで20260122120000_add_order_table.sqlを作成
  3. 開発者Aが先にdevelopにマージ
  4. 開発者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の型安全性を保ちながら、データベーススキーマを厳密に管理できます。

これらの機能を適切に使えば、データベースマイグレーションに関する多くの問題を解決できます。特に、複数人での開発や、複数環境(開発/ステージング/本番)での運用において、その真価を発揮しますね。

この記事がどなたかの参考になれば幸いです。

参考