fastmigrateの紹介
本文の状態
日本語全文を表示中
詳細モードで約21分の本文を読めます。
同じ出来事の情報源
この情報源を基点に整理
Answer.AI
Answer.aiは、SQLite専用でORM不要のPython製データベースマイグレーションツール「fastmigrate」を公開した。直接SQLiteを扱い簡易な運用を目指す開発者向けであり、詳細は公式リポジトリで確認できる。
Source Article
元記事を日本語で読む
本文に関係しない購読案内、埋め込み通知、サイト内プロモーションは除いています。
注意
TLDR: この投稿では、Python のデータベースマイグレーションツールである fastmigrate について紹介します。SQLite に焦点を当てており、特定の ORM ライブラリは必要としません。SQLite と直接作業を行い、シンプルさを保ちたい場合に適しています。手順については、fastmigrate リポジトリをご覧ください。
マイグレーションについて話しましょう!

私が言っているのは、あの「移動」のことではありません。
ええと、データベースマイグレーションパターンについて話しましょう。
マイグレーションは、データベース内の変更を管理するための強力なアーキテクチャパターンです。これにより、アプリケーションコードはデータベースの最新バージョンのみを知っていればよくなり、データベース自体を更新するために使用するコードも簡素化されます。
しかし、多くのデータベースヘルパーライブラリが同時に非常に複雑な他の多くのことを行うため、この基本的なパターンのシンプルさが隠れてしまい、見過ごされがちです。
そこで今日、私たちはデータベースマイグレーションのためのライブラリおよびコマンドラインツールである fastmigrate をリリースします。これは、ツール自体をシンプルにすることで、基盤となるパターンのシンプルさを尊重しています。提供されるコマンドは少数です。マイグレーションを単なるスクリプトのディレクトリとして扱います。本質的なアイデアを理解すればよく、余計な専門用語を覚える必要はありません。私たちはこれを気に入っています!
本記事では、データベースマイグレーションとは何か、そしてそれがどのような問題を解決するのかを一般論として説明し、その後、fastmigrate を用いた SQLite でのマイグレーションの実行方法を具体例を通じて解説します。
マイグレーションが解決する問題
マイグレーションが解決する中核的な問題は、アプリケーションを破損させることなくデータベーススキーマ(およびその他の基本構造)を変更しやすくすることです。これを実現するために、アプリケーションコードの変更と同様に、データベースのバージョンを明示的かつ管理可能なものとしています。
そうでない場合に複雑性がどのように蔓延するかを理解するために、アプリ開発における典型的な一連の出来事を考えてみましょう。アプリが初めて実行される際、扱うべき状況は一つだけです。それはまだデータベースが存在せず、作成する必要があるケースです。この時点でのアプリの起動コードは以下のようになるかもしれません:
App v1
db.execute("CREATE TABLE documents (id INT, content TEXT);")
しかし待ってください… ユーザーがその同じアプリを二度目実行したとき、テーブルはすでに存在しています。つまり実際には、コードは二つの可能なケースを処理する必要があります。それはテーブルが存在しないケースと、すでに存在しているケースです。
そこで、次のバージョンのアプリでは、初期化コードを以下のように更新します:
App v2
db.execute("CREATE TABLE IF NOT EXISTS documents (id INT, content TEXT);")
その後、データベースに新しい列を追加することを決定するかもしれません。そのため、アプリの三番目のバージョンでは二行目を追加します:
App v3
db.execute("CREATE TABLE IF NOT EXISTS documents (id INT, content TEXT);")
db.execute("ALTER TABLE documents ADD COLUMN title TEXT;")
しかし、待ってください…もし列が既に存在する場合は、このようにテーブルを変更したくありません。そのため、App v4 ではそのようなケースを処理するためにより複雑なロジックが必要になります。そして、さらに続く問題もあります。
この些細な例さえも、適切に処理されなければバグを生じさせます。実際のアプリケーションでは、テーブル間の関係性を導入し、その後変更を加えていくにつれて、こうした問題はより微妙で多数存在し、ストレスの多いものとなります。なぜなら、間違ったステップを一つ踏むだけでユーザーデータを失う可能性があるからです。
起こるのは、新しいバージョンが出るたびに、データベースの状態が一つだけでなく、ありとあらゆる過去の状態を処理する必要があるため、アプリケーションコードが複雑化していくという事実です。
これを避けるには、データベースの更新を別々に強制し、アプリケーションコードがデータベースから何を期待すべきかを正確に把握できるようにする必要があります。しかし、アプリがデータベースを管理しており、各ユーザーが自分自身のアプリインストールを実行するタイミングを決定できる場合(モバイルアプリ、デスクトップアプリ、あるいはユーザーごとにデータベースを持つ Web アプリの場合など)、これは必ずしも実現可能ではありません。単一のデータベースを持つシステムであっても、別々のデータベース更新を強制することは、管理すべき重要な新しい種類の変更——つまり、アプリケーションコードの変更と繊細に連携させる必要があるデータベース変更——を導入することになります。
これが問題の核心です。デフォルトでは、これらのさまざまなデータベース状態は暗黙的であり、管理されていないからです。
アプリケーションコードにおいて、git コミットはコードのバージョンと、その変更を生み出した内容を明確に指定します。その後、デプロイメントシステムにより、ユーザーが次に目にするアプリケーションのバージョンを正確に制御できます。しかし、データベースにおいては、何らかのシステムがない限り、単に「過去のコードによって生成された、名前のない状態にある」ということしか分かりません。アプリケーションコードを非常にうまく管理するバージョン管理ツールやデプロイメントツールは、自動的にアプリケーションが次に参照するデータベースのバージョンを制御するものではありません。
マイグレーションがこの問題をどう解決するか
データベースマイグレーションパターンは、2 つの主要な対策によってこの問題を解決します:
第一に、マイグレーションに基づいたデータベースバージョンの定義です。名前のないデータベース状態について推論するのではなく、データベースの明示的なバージョン管理を導入します。
これをどのように行うのでしょうか?マイグレーションスクリプトを用います。マイグレーションスクリプトは、孤立した単一目的のスクリプトであり、その唯一の仕事は、データベースをあるバージョン(例:5)から次のバージョン(例:6)へ移行させることです。
Fastmigrate はこの仕組みをシンプルに保ち、生成されるデータベースバージョンに基づいてスクリプト名を付与します。例えば、0006-add_user.sql という名前のスクリプトは、データベースバージョン 6 を生成する唯一のスクリプトでなければなりません。根本的な意味において、マイグレーションスクリプト内のバージョン番号は、認識されるデータベースバージョンのセットを定義します。したがって、git のコミットログを見るように、これらのバージョンを生成したスクリプトをリストアップすることで、過去のデータベースバージョンを確認できます:
$ ls -1 migrations/
0001-initialize.sql
0002-add-title-to-documents.sql
0003-add-users-table.sql
この構造化されたアプローチは、次の重要な施策を可能にします。
第二に、アプリケーションコードが特定のデータベースバージョンを対象とするように記述することです。データベース進化のコードをこれらのマイグレーションスクリプトに移管することで、アプリケーションコードはデータベースの変更について気にする必要がなくなり、最新のデータベースバージョンのみを対象とすればよくなります。
アプリケーションは、fastmigrate などのマイグレーションライブラリに依存して、必要なすべてのマイグレーションを実行させることができます。これには、開発環境で新規インスタンスを起動する際に、ゼロからすべてのマイグレーションを再実行して最新のデータベースバージョンを作成する場合もあれば、直近のデータベースバージョンを最新の状態に更新するために最新のマイグレーションのみを適用する場合もあります。あるいは、その中間のケースもあり得ます。重要なのは、アプリケーション側がそれを気にする必要がないという点です。
簡素化の程度を測る一つの方法は、システムの異なる部分が処理しなければならないケース数がどれだけ減ったかを数えることです。
マイグレーションを行う前、アプリケーションコードは、すべての可能な過去のデータベース状態を処理する責任を負っていました。それらの状態が何であるかを覚えて理解するために、ますます注意深い配慮が必要となる場合でもです。
マイグレーションの後では、すべてが明示的であり、読みやすく、構造化されています。アプリケーションは単一のデータベースバージョンのみと連携する責任を負います。そして、各データベースバージョンには、それを前の1つのバージョンから生成するスクリプトが正確に1つだけ存在します。(あまりにも清潔!あなたもため息をつきたくなりませんか?あああ…)
機能
マイグレーションなし
マイグレーションあり
DB 状態
数えきれず、名前もない
image の明示的なバージョン
DB 管理
なし
image の分離されたマイグレーションスクリプト(各バージョンごとに1つ)
アプリ要件
アプリはすべての DB 状態をサポートし、DB 変更を管理する必要がある
アプリは最新の DB バージョンのみをサポートすればよい
fastmigrate の使い方
前の例をもう一度追って、これが fastmigrate でどのように機能するかを見てみましょう。
進化するデータベーススキーマのロジックをアプリケーションの起動処理に埋め込むのではなく、一連のマイグレーションスクリプトを定義します。これらのスクリプトは SQL ですが、Python やシェルスクリプトを使用することもできます。その後、アプリケーションは fastmigrate の API を使用して、必要に応じてそれらのスクリプトを実行し、データベースを自動的に最新の期待されるバージョンに更新します。
最初のマイグレーションスクリプトでテーブルを作成します。migrations/ というディレクトリを作成し、その中に 0001-initialize.sql というファイルを入れてください。
-- migrations/0001-initialize.sql
CREATE TABLE documents (
id INTEGER PRIMARY KEY,
content TEXT
);
0001 というプレフィックスが鍵となります。これは、このスクリプトが最初に実行されるべきものであり、同時にデータベースのバージョン 1 を生成することを示しています。
PyPI からインストールしてアプリで使用できるようにするには、pip install fastmigrate を実行してください。
これでアプリケーションの起動コードは、fastmigrate を利用してデータベースの作成および/または更新を行うことができます。app.py という名前のファイルに、以下のように入力します:
from fastmigrate.core import create_db, run_migrations, get_db_version
db_path = "./app.db"
migrations_dir = "./migrations/"
バージョン管理されたデータベースが存在することを保証する。
データベースが存在しない場合は作成され、バージョン 0 に設定される。
データベースが既に存在する場合は何もしない。
create_db(db_path)
migrations_dir から未適用のマイグレーションをすべて適用する。
success = run_migrations(db_path, migrations_dir)
if not success:
print("Database migration failed! Application cannot continue.")
exit(1) # または、アプリ固有のエラーハンドリング処理を行う
このポイント以降、アプリケーションコードは 'documents' テーブルが 0001-initialize.sql で定義されたとおりに確実に存在すると安全に仮定できる。
データベースは現在バージョン 1 です。
version = get_db_version(db_path)
print(f"Database is at version {version}")
この Python コードを初めて実行する際、create_db() がデータベースを初期化し、メタデータを挿入して管理対象のデータベースであることを示すとともに、バージョン 0 に設定します。これは、現在のバージョンと管理対象データベースであることを示す小さな _meta テーブルを追加することで実現されます。
次に、run_migrations() 関数は 0001-initialize.sql を検出します。バージョン 1 がデータベースの現在のバージョン 0 より大きいため、この関数がスクリプトを実行し、データベースのバージョンを 1 にマークします。
その後の実行では、新しいマイグレーションスクリプトが追加されていない場合、run_migrations() はデータベースがすでにバージョン 1 に達していることを確認し、それ以上の処理は行いません。
python3 app.py でアプリを実行すると、何度実行してもデータベースはバージョン 1 であると報告されます。また、ディレクトリ内に作成されたデータベースファイル data.db も確認できます。
では、スキーマの進化についてはどうでしょうか?
ドキュメントテーブルに title カラムが必要だと判断した場合、そのカラムを追加するマイグレーションスクリプトを新たに追加するだけで済みます。
この変更により、データベースのバージョン 2 が定義されます。migrations ディレクトリには、0002-add-title-to-documents.sql という名前のファイルを追加してください。
-- migrations/0002-add-title-to-documents.sql
ALTER TABLE documents ADD COLUMN title TEXT;
重要な点は、アプリケーションの起動コードは変更されないことです。上記で示した Python のスニペットがそのまま維持されます。
このコードが、以前バージョン 1(つまり 0001-initialize.sql しか適用されていない状態)にあったデータベース上で実行されると、以下の手順が行われます:
create_db(db_path) がデータベースの存在とバージョン 1 であることを確認します。
run_migrations() は migrations/ ディレクトリをスキャンし、0002-add-title-to-documents.sql を発見します。このスクリプトのバージョン(2)がデータベースの現在のバージョン(1)より大きいため、新しいスクリプトが実行されます。
正常に実行されると、fastmigrate はデータベースのバージョンを 2 にマークします。
これらの fastmigrate コール後に実行されるアプリケーションコードは、now documents テーブルに id、content、および新しい title カラムがあると想定できるようになります。
python3 app.py でアプリを再度実行すると、データベースがバージョン 2 にあると報告されます。
もし内部でこれがどのように動作しているか気になる場合は、決して神秘的な仕組みではありません。fastmigrate は_meta テーブルを追加することでデータベースにマークを付けます。これは sqlite3 実行ファイルを使用して直接確認できます:
$ sqlite3 app.db .tables
_meta documents
中身を確認すると、バージョンが 2 になっていることがわかります:
$ sqlite3 app.db "select * from _meta;"
1|2
ただし、これは実装の詳細に過ぎません。重要な点はアプローチの転換です。
複雑な条件分岐ロジックは、アプリケーションのメイン起動シーケンスから完全に削除されました。
スキーマ変更は、小さく明確に名前付けされたバージョン管理された SQL スクリプトに隔離されます。
データベーススキーマが進化しても、アプリケーションのコア起動ルーチン (create_db(), run_migrations()) は安定しています。
残りのアプリケーションコード、つまり実際にデータベースを使用する部分は、常に最高番号のマイグレーションスクリプトで定義された単一の最新スキーマバージョンを前提として記述できます。古いデータベース構造に対する条件分岐パスは不要です。
この「アペンドオンリー」アプローチでは、後続の変更に対して常に新しい番号の大きいスクリプトを追加するため、データベースの進化が明示的かつ管理されやすく、統合も容易になります。ターゲットとなるスキーマバージョンに到達する責任は fastmigrate に委譲されます。
コードをバージョン管理システムにチェックインする際は、新しいデータベースバージョンを定義するマイグレーションスクリプトと、その新しいデータベースバージョンを必要とするアプリケーションコードの両方を必ず含めるように注意してください。そうすれば、アプリケーションコードは常に必要なデータベースバージョンを正確に参照することになります。
コマンドラインでのテスト
新しいマイグレーションスクリプトをアプリに統合する前に、もちろんテストを行う必要があります。マイグレーションスクリプトは単独で実行できるように設計されているため、これは非常に簡単です。対話型で実行をサポートするために、fastmigrate はコマンドラインインターフェース (CLI) も提供しています。
アプリが作成したデータベースを検査したい場合は、バージョンチェックコマンドを実行します:
$ fastmigrate_check_version --db app.db
FastMigrate version: 0.3.0
Database version: 2
CLI コマンドの名前が API と一致する場合、それらは全く同じ動作を行います。fastmigrate_create_db は fastmigrate.create_db と同様に動作し、fastmigrate_run_migrations は fastmigrate.run_migrations と同様です。
例えば、以下のコマンドを実行して、空の管理用データベースを作成し、その上でマイグレーションを実行できます:
$ fastmigrate_create_db --db data.db
データベースを data.db に作成中
data.db にバージョン 0 の新しいバージョン管理付き SQLite データベースが作成されました。
$ fastmigrate_run_migrations --db data.db --migrations migrations/
マイグレーション 1 を適用中:0001-initialize.sql
✓ データベースがバージョン 1 に更新されました (0.00 秒)
マイグレーション 2 を適用中:0002-add-title-to-documents.sql
✓ データベースがバージョン 2 に更新されました (0.00 秒)
マイグレーション完了
• 2 つのマイグレーションが適用されました
• データベースは現在バージョン 2 です
• 合計所要時間:0.00 秒
学ぶべき新しいことはありません!
新しいマイグレーションを導入する際の推奨ワークフローの詳細な手順については、マイグレーションを安全に追加する方法に関するガイドをご覧ください。
また、fastmigrate の外で開始されたデータベースを取得し、管理対象のデータベースとして登録するためのガイダンスも用意されています。技術的にはこれは単にデータベースのバージョンを示すプライベートメタデータ(metadata)を追加するだけの作業ですが、ツールは登録しようとしているデータベースと同等のデータベースを初期化する必要があるため、ドラフト版の 0001-initialize.sql マイグレーションスクリプトを生成することで、開始時のサポートを提供します。この生成されたスクリプトはあくまでドラフトであり、ご自身のデータベースに正確に適しているかどうかを手動で必ず確認する必要があります。
シンプル=明確=落ち着き
もう一度その地図を見て、私たちの祖先がエアコンもポッドキャストも AI チャットボットもない状態で数千マイルを旅したことを考えてみてください。それは過酷なものでしたが、はい、私たちはそれほどひどい状況にあるわけではありません。
しかしながら、本番環境のデータベースの進化を管理するのはストレスがかかります。
これは自然なことです。なぜなら、それはユーザーのデータだからです。ほとんどのソフトウェアの全目的は、そのデータを処理・保存することにあります。したがって、もしデータベースを誤操作すれば、ソフトウェアはその存在する主な理由において失敗したことになります。
そのようなストレスに対する解毒剤は明確さです。自分が何をしているのかを知りたいのです。
誰かが git のコミットをハッシュ値で参照したときに感じる温かい安心感を想像してみてください。(んー。)その感覚が生まれるのは、ハッシュ値が曖昧さを含まないからです。git に 2 つのコミットハッシュの間に変更されたファイルを計算させれば、答えの意味が正確にわかります。データベースについても同じような明確さを持ちたいものです。
マイグレーションパターンは、データベースに単純なバージョン番号を持たせることでその明確さをもたらします。この番号により、データベースがどの状態にあるかがわかり、したがってアプリケーションが期待できることが正確に特定できます。
そして、これはシンプルなアイデアなので、必要なツールもシンプルで十分です。
だからこそ fastmigrate は、create_db、get_db_version、run_migrations という数個の主要なコマンドのみを導入し、ファイルの一覧表示や整数の意味解釈など、すでに知っている機能に依存しています。
一方、多くの既存のデータベースツールは複雑です。なぜなら、それらは他にも多くの機能を備えているからです——オブジェクトリレーショナルマッパー(ORM)、テンプレートシステム、さまざまなバックエンドへのサポート、異なる構文を持つ複数の設定ファイルに対する要件など。もしあなたのシステムの複雑さがこれらすべてを必要とするレベルにまで成長しているなら、それがまさに必要なものです。
しかし、システムをシンプルに保つことができるのであれば、シンプルな解決策の方がより良い結果をもたらします。理解しやすく、使いやすく、頭の中や手の中で扱いやすいものになります。ニンジンをおろすとき、あなたは鋭い包丁を望みますか?それとも、特別なニンジンスライサーアタッチメント付きのフードプロセッサーを望みますか?そのアタッチメントを取り付ける方法を理解するために、マニュアルを読まなければならないようなものです。
fastmigrate は、そのような鋭い包丁になることを目指しています。あなたがそれを明確さと自信を持って扱えるよう願っています!
関連記事
今日のまとめ
AIデイリーブリーフで今日の重要ニュースをまとめ読み