FuelPHP Advent Calendar 2015 の 9 日目担当の @sji_ch です。昨日は @tanaka8com さんの「初心者向け FuelPHP チュートリアルサイトを 1 年半運営した結果」でした。
この記事では FuelPHP のマイグレーション機能が何なのかの簡単なおさらい、少しだけ突っ込んだ内部挙動やアンドキュメンテッドな機能の話、おまけで開発版 1.8/develop で新しく追加された機能について、ざっくり書きます。
対象読者は以下のような人です。
- FuelPHP のマイグレーションを使ったことがあり、変な使い方をして困ったけどよく分からないうちに解決してしまった的な経験があるので、細かい挙動をもっと知りたい、でもソース読むのはかったるいという人
- そこそこ暇な人
「変な使い方をして困った」というのは、たとえば
- Git で他の開発者と一緒に開発していて
- 他の人が追加したマイグレーションファイルと自分の追加した分でバージョンナンバーが衝突してしまい
- またそれをマージする際、既にインストール済みのを down しないままリネームしてしまい
- 無理やりでも辻褄合わせがしたいけど
- DB と 設定ファイル両方でインストール済みマイグレーションの情報を持っていて、どっちが何用だかよく分からなくて心が虚無に呑まれる
とか、そういう時に中身のことがちょっとだけ知りたくなります。
対象とする FuelPHP のバージョンは現在の最新安定版 1.7.3 です。1.8/develop にのみ含まれる機能の話もちょっとだけ含まれています*1。
- *1先日 PHP7 がリリースされ、これに対応するための微調整が 1.8/develop にも入っています。こないだ FuelPHP の中の人が「PHP7 出たらこっちも v1 系の新しいのリリースしようかな」みたいなことを言ってたので、近々この機能の含まれた安定版も出るかもです
そもそもマイグレーション機能って何だっけ
migration
【名詞】
1【不可算名詞】 [具体的には 【可算名詞】] 移住,転住; 渡り.
2【可算名詞】 [集合的に] 移住者群,移動する動物の群.
FuelPHP のマイグレーション機能は、そのままではソースコードに直接あらわれない変更をソースコード上に記録し、変更を取り消したり再現したりする仕組みです。
ソースコードの変更は Git などのバージョン管理システムの利用によって追跡可能ですが、たとえば DB のスキーマの変更を、各 DBMS が内部的に使う DB ファイルのバージョンを管理する、というような形で扱うのは不便です。DBMS の移行や、引っ越し等による環境差異の吸収も難しくなったりします。
このような変更をマイグレーションファイルとして記述することで、運用中のプロダクトについて開発環境で加わったデータの変更を本番環境で再現する、というような、バージョン A から バージョン B への移行を容易に扱うことができるようになります。
FuelPHP のマイグレーションファイルはデフォルトの設定では app/migrations 下へ(フォルダ名は app/config/migrations.php で指定可能)、PHPファイルとして作成します。
また DB スキーマを変更するマイグレーションファイルは、oilで追加することも可能です。
ところで、マイグレーションで可能なのは実は DB スキーマの変更だけではありません。マイグレーションファイルは基本的に up() と down() というメソッドを持つただのクラスファイルで、データの CRUD はもちろん、ファイルやディレクトリの作成、外部ツールのセットアップなど、FuelPHP に可能なことなら何でもできます*1。スクリプトの起動がそのバージョンのマイグレーションファイルで up、down するタイミングのみになるため、いつでも必要なときに起動できるタスク機能の方が使い勝手のよい場面は多いでしょうが、バージョンの移行に伴う変更についてはマイグレーションを使うのが自然なケースもあることでしょう
- *1FuelPHP の中の人は「コーヒーいれる以外のことは何でもマイグレーションでやるぜ」なんて冗談を言っていたりもします
中身について
FuelPHP 内部で、マイグレーション機能は大きく分けてコアの Migrate クラスと oil の Migrate タスクの 2 つのパーツに分かれています。Migrate クラスがマイグレーションのための機能を全て持ち、Migrate タスクはそのコマンドラインインターフェースとなります。
Migrate クラス
Migrate クラスは current()、latest()、version() の 3 つのドキュメントされた公開メソッドを持ち、また up()、down()という 2 つのドキュメントされていない公開メソッド、そして初期化用の _init() メソッドを持ちます。FuelPHP の デフォルトで提供されている oil の migrate タスクは、このクラスを利用することでマイグレーション機能を提供しています。コントローラから直接 Migrate クラスを使ってマイグレーション機能へアクセスするようなことも、一応は可能です*1。
- *1可能というだけで、多数のユーザから同時にマイグレーション機能へアクセスし得るような作りはおすすめしません。内部構造的に複数リクエストからアクセスがあった際、それぞれの間でインストール済みマイグレーションについての情報が同期されないため、不可解な挙動を引き起こすかもしれません
_init()
Migrate クラスは _init() メソッドにより、最初にオートローダから読み込まれた時点で DB から migration テーブルの内容を読み込みます。このデータは各メソッドでマイグレーションファイル用フォルダから実行するファイルを探索する際、up であればインストール済みのマイグレーションを実行対象から除外し、down であれば対象に含める、というような判断に使われ、スクリプト実行中のマイグレーション操作にあわせて内容が増減します。
migration テーブルの読み込みはプロセスへオートローダで Migrate クラスが読み込まれた時点でのみ起こり、その後のテーブルの状態変化そのものはプロセス終了まで追跡されず、実行中の PHP スクリプト内で Migrate クラスを通して行われた変更以外は検知されないことに注意してください。おそらくやろうとする人はあまりいないでしょうが、マイグレーションを並列で実行しようとするようなことはトラブルの元となり得ます。
version()、latest()
http://fuelphp.jp/docs/1.8/classes/migrate.html#/method_latest
version() メソッドは現在のバージョンから指定されたバージョンへ向けて、マイグレーションファイルを逐次に実行していくのに使います。latest() はversion() の薄いラッパーで、現在のバージョンから最新バージョンのマイグレーションファイルまでを実行していきます。
「現在のバージョン」は、設定ファイル migrations.php の version から取得されます。app や package、module といった各マイグレーション対象について、最後に書かれているものが「現在のバージョン」として使われます。
- 「現在のバージョン」と「指定されたバージョン」を比較することで、
- これからやるのがバージョンの up か down かを判定し、
- マイグレーションファイル用フォルダ内のファイルを列挙して(GlobIterator による)、
- ファイル名の先頭へアンダースコア区切りで置かれたバージョン番号が「現在のバージョン」と「指定されたバージョン」の間にあり、
- かつ down なら DB のマイグレーションテーブルでインストール済みとして記録されているもの、up なら記録されていないもの
が実行対象のマイグレーションファイルとして選び出され、順番に(down であれば逆順に)実行されていくことになります。
実行対象マイグレーションファイルのリストは内部的に uksort() を strnatcasecmp() で呼ぶことでソートされており、実行順序は自然順に並ぶことが保証されています。
今のところドキュメントには書かれていないのですが、version() は 4 つ目の引数として(latest() も 3 つ目の引数として)、真偽値を受け付けます。デフォルトは false です。true が渡された場合は設定ファイルの「現在のバージョン」を無視し、指定バージョンまでのマイグレーションファイルを最初から実行していく挙動となります。ただし DB のマイグレーションテーブルへインストール済みかどうかのチェックは、デフォルトの場合と変わらず行われます。
この機能は oil からは oil migrate の際に –catchup オプションを使うことで利用できます。
oil migrate –catchup
current()
current() メソッドは設定ファイルから version() の場合と同様に「現在のバージョン」を取得し、この「現在のバージョン」へ向かって、マイグレーションファイル用フォルダの内容を最初から実行していきます。version() の場合と同様、DB のマイグレーションテーブルへインストール済みかどうかのチェックが、実行するか否かの判定に使われます。
たとえば Git で共同開発をしていて、pull した際に設定ファイルの migrations.php が手元のローカル開発環境の DB よりも先へ進んだ場合など、設定ファイルのバージョンへ DB の状態を合わせるのに、oil migrate:current を通して使います。
up()、down()
それぞれ version() の up、down 限定版です。version() と同様にマイグレーションファイル用フォルダの中から対象を探し、第 1 引数でバージョンが指定されている場合はそのバージョンまで、指定されていない場合は 1 つ分だけ up や down を行います。「現在のバージョン」は version() と同様に取得されます。公式ドキュメントには記載がありませんが、ソースコード上には存在し、oil の Migrate タスクからも使われているメソッドです。
oil からは oil migrate:up や oil migrate:down を通して使います。
マイグレーションファイルの新機能
話は変わりますが、先月あたり、1.8/develop でマイグレーションファイルに before / after が追加されました。
発端は github の以下のスレッドです。
DBUtil: transactions have no effect
スレッド自体はある人が「DBUtil の操作内容がロールバックできない」という issue を上げたもので、おそらく MySQL のような DDL をトランザクションに入れられない DBMS を使ったためなのではないかと思いますが、その話自体とは別に話の流れの途中で、
- 「そういえばマイグレーションの up / down を自動的にトランザクションでラップするような機能があると嬉しいかも」
- 「あーでも考えたら MySQL とか DDL ダメじゃん、でも PostgreSQL とかはいけるよね」
- 「before() / after() 導入して、各マイグレーションファイルで共通化しやすくすればいいんじゃね」
- 「ついでに before() が return false したらマイグレーション実行スキップするとか、事前条件のバリデーションかけられるようにしようぜ」
- 「いいじゃんそれ、after() の方はどうするよ?」
- 「はっきりした使いみちは思い浮かばないけど、マイグレーションは DB 触るだけのものでもないし、事後に失敗/成功チェックすると便利な時は何かしらあるんじゃね? before() との整合もとれるし」
- 「じゃあ、up で false 返したら down、down で false 返したら up で自動的にリバートかけるようにしたらいいかね」
- 「じゃあそれで」
的な会話が中の人たちの間で為され、実装されたという形です。
trait を作るなりマイグレーションファイル用の基底クラスを用意するなりすることで、全てのマイグレーションの実行前後で共通で行うような処理が、比較的気軽に書けるようになりました。
現時点だと github の fuel/docs の方ではこの機能についての記述が追加されているのですが、公式サイトのドキュメントの方にはまだ反映されていないようで、この機能については記載がありません。
http://fuelphp.com/docs/general/migrations.html
よく見たらあったらしい!
http://fuelphp.com/dev-docs/general/migrations.html#/prep_migration
一方、日本語ドキュメントの方は 1.8/develop 用の翻訳も公開されているため、公式ドキュメントにない最新情報が翻訳版の方には載っている、という少し面白い状態になっています(訳文の方はちょっと修正が要りそうですね、PR 出そうかな……)。
http://fuelphp.jp/docs/1.8/general/migrations.html#/prep_migration
Fuel v2 は中の人が具合悪めであまりバリバリは進められていないそうなのですが、意外と v1 の方に細かい改良や新機能が盛り込まれてたりします。冒頭で少し触れた通り、PHP7 が出たのにあわせ v1 の PHP7 対応の安定版もリリースされる筈で、比較的最近の変更はそちらへ含まれることになりそうです。新しいバージョンが出るのが今から楽しみです。