はじめに
「マイグレーション、また失敗した...」
正直、Driftを使い始めた頃はテーブル定義を変更するたびにドキドキしていた。カラム追加くらいなら問題ないけど、テーブル削除や型変更が絡むと途端に不安になる。
実際、Driftの公式ドキュメントにはこう書いてある:
"Manual migrations are error-prone and can lead to data loss."
(手動マイグレーションはエラーが発生しやすく、データ損失につながる可能性があります)
公式がここまで言うなら、何か理由があるはず。この記事では、従来方式の何が問題で、stepByStep()がどう解決するのかを解説する。
この記事でわかること
- 従来のif文方式が抱える「最新スキーマ問題」
-
stepByStep()が各バージョンのスキーマを保持する仕組み - 基本コマンド(
schema dump/schema steps)の使い方 - 推奨ワークフロー
対象読者
- Driftを使っているがマイグレーションで困っている人
- 長期運用アプリを開発している人
- 「本番でマイグレーション失敗」を経験したくない人
従来方式の問題点
問題1: Driftは最新スキーマしか認識しない
多くのチュートリアルで見かけるif文方式。一見シンプルだけど、致命的な問題がある。
// database.dart
@override
int get schemaVersion => 3;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (m, from, to) async {
// v1 → v2: usersテーブルにbirthDateカラム追加
if (from < 2) {
await m.addColumn(users, users.birthDate); // ❌ エラー!
}
// v2 → v3: usersテーブルを削除
if (from < 3) {
await m.drop(users);
}
},
);
何が問題か?
- v3で
usersテーブルを削除した - Driftは最新のスキーマ(v3) しか認識しない
- v3時点では
usersテーブルは存在しない - → v1→v2のマイグレーションで
usersを参照できずコンパイルエラー
結局、「過去のバージョンで必要だったテーブル」を削除すると、そのテーブルを参照するマイグレーションコードが動かなくなる。これがif文方式の落とし穴。
問題2: 実際に報告されている事例
GitHub Issue #1174では、こんな報告がある:
- v1→v2マイグレーション中に、v3で追加された
third_colが参照されて「no such column」エラー - 原因:システムが最新スキーマ(v3)を使用してマイグレーションを実行してしまう
これ、デバッグがめちゃくちゃ難しい。なぜなら「正しいコード」を書いたはずなのに、後からスキーマを変更しただけで壊れるから。
問題3: コードの保守性低下
削除したテーブルのマイグレーションコードをソースから削除できない。v3でusersを削除しても、v1→v2のコードは残り続ける(削除するとコンパイルエラー)。
長期運用していると、「もう使わないテーブル」のマイグレーションコードがどんどん溜まっていく。
stepByStep()が解決すること
ポイント: 各バージョンのスキーマを個別に保持
stepByStep()を使うと、各マイグレーション関数がその時点のスキーマにアクセスできる。
onUpgrade: stepByStep(
from1To2: (m, schema) async {
// schemaはSchema2(v2時点のテーブル定義)
// v3でusersが削除されても、Schema2にはusersが存在する
await m.addColumn(schema.users, schema.users.birthDate); // ✅ OK!
},
from2To3: (m, schema) async {
// schemaはSchema3(v3時点のテーブル定義)
await m.drop(schema.users);
},
);
公式ドキュメントの説明:
"Each fromXToY function has access to the schema it's migrating to. So, schema.users is available in the migration from version one to two, despite the users table being deleted afterward."
(各fromXToY関数は、マイグレーション先のスキーマにアクセスできます。そのため、usersテーブルが後で削除されても、v1→v2のマイグレーションではschema.usersを使用できます)
from1To2ではSchema2を参照し、from2To3ではSchema3を参照する。後のバージョンでテーブルが削除されても、過去のマイグレーションには影響しない。これがstepByStep()の強み。
利点まとめ
| 項目 | 従来方式 | stepByStep方式 |
|---|---|---|
| スキーマ参照 | 最新のみ | 各バージョン個別 |
| 削除済みテーブル | 参照不可 | 参照可能 |
| 型安全 | ❌ | ✅ |
| IntelliSense | ❌ | ✅ |
| 古いバージョンのテスト | 困難 | 容易 |
基本コマンドの使い方
stepByStep()を使うには、スキーマのスナップショットを管理する必要がある。2つのコマンドを覚えればOK。
Step 1: 初期スキーマをエクスポート
dart run drift_dev schema dump lib/database.dart drift_schemas/
- 現在のスキーマを
drift_schema_v1.jsonとして出力 -
schemaVersionの値がファイル名になる
Step 2: スキーマ変更後、再度エクスポート
テーブル定義を変更したら、schemaVersionをインクリメントして再度エクスポート。
// database.dart
@override
int get schemaVersion => 2; // 1 → 2 に変更
dart run drift_dev schema dump lib/database.dart drift_schemas/
# → drift_schema_v2.json が生成される
Step 3: stepByStepコードを生成
dart run drift_dev schema steps drift_schemas/ lib/database.steps.dart
このコマンドは:
-
drift_schemas/内のすべてのJSONを読み込む -
Schema1,Schema2, ... クラスを生成 -
stepByStep()ヘルパー関数を生成
生成されるファイル
// database.steps.dart(自動生成)
final class Schema1 extends VersionedSchema { ... }
final class Schema2 extends VersionedSchema { ... }
MigrationStrategy stepByStep({
required Future<void> Function(Migrator m, Schema2 schema) from1To2,
// ...
})
実装例
database.dart
import 'database.steps.dart';
@DriftDatabase(tables: [Users, Posts])
class AppDatabase extends _$AppDatabase {
@override
int get schemaVersion => 3;
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
onUpgrade: stepByStep(
from1To2: (m, schema) async {
await m.addColumn(schema.users, schema.users.birthDate);
},
from2To3: (m, schema) async {
await m.createTable(schema.posts);
},
),
);
}
型安全の威力
// IntelliSenseが効く
schema.users.bi // → birthDate が候補に表示される
// タイポはコンパイルエラー
await m.addColumn(schema.users, schema.users.birthdata);
// ^^^^^^^
// error: The getter 'birthdata' isn't defined
従来方式だと、タイポは実行時エラー。stepByStep()なら、IDEが即座に教えてくれる。
推奨ワークフロー
1. テーブル定義を変更
2. schemaVersion をインクリメント
3. dart run drift_dev schema dump ...
4. dart run drift_dev schema steps ...
5. stepByStep()にfromXToY関数を追加
6. テスト実行
慣れると3分くらいで終わる。
ベストプラクティス
1. スキーマJSONはGit管理
drift_schemas/
├── drift_schema_v1.json ← コミットする
├── drift_schema_v2.json
└── drift_schema_v3.json
チーム開発では、全員が同じスキーマ履歴を共有できる。
2. 古いバージョンからのテスト
v1→v3直接アップグレードするユーザーもいる。stepByStep()なら各バージョン間のマイグレーションを個別にテスト可能。
3. (+α) スクリプト化で効率化
毎回2つのコマンドを打つのが面倒なら、スクリプト化すると便利。
#!/bin/bash
# scripts/schema.sh
set -e
dart run drift_dev schema dump lib/database.dart drift_schemas/
dart run drift_dev schema steps drift_schemas/ lib/database.steps.dart
echo "✅ Done!"
まとめ
- 従来方式の問題: Driftは最新スキーマしか認識しない → 削除済みテーブル参照不可
- stepByStep()の解決策: 各バージョンのスキーマを個別に保持
- 公式推奨: 「手動マイグレーションはデータ損失のリスクあり」と明記
- 長期運用アプリには必須のパターン
最初のセットアップは少し面倒だけど、一度環境を整えれば「マイグレーション失敗で本番クラッシュ」みたいな事故を防げる。特に長期運用するアプリでは必須だと思う。
「うちではこうしてる」とか「ここ違くない?」があったら、コメントで教えてほしい。
宣伝
この記事で紹介したパターンは、フリーランス向け業務管理アプリ「Freelance One」で実際に使っている。
稼働記録、契約管理、請求書作成など、フリーランスエンジニアに必要な機能を1つにまとめたアプリ。よかったら触ってみてほしい。
📱 App Store: https://link.nekoder.com/freelance-one-appstore
記事の感想や「うちではこうしてる」があれば、Xで教えてもらえると嬉しい。
𝕏 (旧Twitter): @HaruyaNekoder
参考文献
- Drift公式: Migrations
- Drift公式: Step by step
- GitHub Discussion #3156 - 既存プロジェクトでstepByStep導入時の注意点
