どうもこんばんわ、日々お世話になったり、便利だと感じたOSSを紹介していく、OSS紹介 Advent Calenderの21日目の記事です。


GitDDLとは

  • あるDBスキーマから別のDBスキーマに変更を行う際(=migration)を行う時に使うCPANモジュール
  • Railsなどのmigrationとは違ったアプローチで行う
  • 中に使われている魔法のようなモジュールSQL::Translator::Diffの紹介もします

DB migrationとは

MySQLなどのRDBMSを用いたアプリケーションを運用していくと、「新しくテーブルを追加したい」「カラムを増やしたい」「インデックスを足したい」などの場面に遭遇することがあります。テーブルを追加するだけであれば、新しくCREATE文を発行するだけで良いのですが、カラムを増やしたいだとかインデックスを足したいとなると、ALTER文を打たなければなりません。

ところで、普段ORMを使っていると、SQLからの距離が遠くなっていくのを感じます(個人差があります)。SELECTなどのDML文はORMに精通していれば、このステートメントを実行すると裏でどういったSQLが発行されるかを想像することが出来ますし、そうでなくてもログをとったり、中には特定ページ内で発行されたSQLが下ににょろっと出て来るフレームワークもありますよね? そうやって我々はSQLとの距離をつなぎとめることが出来るのです。

しかし、CREATE文やALTER文などのDDLとなると話は違ってきます。早々頻繁に打たないものですから、彼らとの距離はどんどん離れていきます。何故離れていくか。大体のフレームワークは彼らを勝手に手なづけてこちらに寄越してくれないからです。

このフレームワークやORMが持つDDLを自動で発行する仕組みのことを全般的にmigrationと呼びます(呼ばれている気がします)。

ほとんどのORMはDBスキーマを知っています。でなければスムーズなObject-Relational Mappingは実現できないからです。DBスキーマを知っているということは、彼らはスキーマの管理を自分の手元に置きたいと思うでしょう。自分の管理外で勝手にカラム追加なんかされてしまっては、とたんにパニクってしまうからです。

重厚なORMであればあるほど、スキーマはDSL上で管理されているように思えます。PerlであればDBIx::Schema::DSLが挙げられます。これはDSLからDDLを吐く単体のモジュールですが、これをORMであるAnikiが利用しています。DBIx::ClassにもそういったDSLが存在します。

ところで実際に彼らのMigrationはどのように行われているでしょうか。ここで一例としてRails migrationを紹介します。

Rails migrationのDDL管理はいわば積み上げ式です。基本のCREATE文を発行するcreate_tableがあります。あなたがもし何か変更を加えたい時、例えばカラムを追加したい時には、ファイルを作って独自のDSLでadd_column :table_name, :column_name, :text, after: :before_column_nameと書きます。これを元にrails migrationコマンドはALTER文を生成してあなたのアプリケーションのデータベースに適用するのです。

GitDDLのmigration方法

僕が見てきた限りほとんどの変更時のDDL文を生成できるライブラリやORMは、この積み上げ方式でしたが、GitDDLはこの方式とはまったく違う方法を取ります。

GitDDLは現在適用されている完全な形のCREATE文と、適用したい今のテーブルの形を示したCREATE文から直接ALTER文を生成します。これは2つのCREATE文同士の差分をALTER文として出力するとも言えます。

GitDDLの仕組みを順を追って説明すると、

  1. 適用先DBの中のgit_ddl_versionからコミットハッシュを取り出し、gitコマンドを実行してそのリビジョン時のDDL(完全な形のCREATE文が連なった形式)を取り出します
  2. 適用後の形を示した完全な形のCREATE文が入ったDDLを読み取ります
  3. この2つをSQL::Translator::Diffに投げます
  4. するとALTER文になって帰ってきます
  5. これをDBに対して適用します
  6. 現在のgitのコミットハッシュをgit_ddl_versionに書き込みます

まあつまり、SQL::Translator::Diffが魔法のようなことをしているわけです。

SQL::Translator::Diffの中身

どうやってそれやるのかというと、実際にDDLをパースして、ASTに変換し、2つのASTの間の差分をとりだして、ALTER文に戻すというのをやっています。文字にすると簡単ですが、途中で追っかけるのを諦めるぐらい結構大変なことになっています。

r7kamuraさんが書かれたSQL::Translator::Diffを読んだ記事があったのですが、失われていました……。

GitDDL/SQL::Translator::Diffの欠点

で、これで僕は4年ぐらいWebアプリを運用しているんですが、ありがたく使わせていただいた上で色々欠点があります。

  • テーブルが多くなると差分を計算するのにかなり時間がかかる
    • だいたい現在400ぐらいテーブルがあって、スキーマ適用に5分か10分ぐらいかかっている。これはインデックスを貼る時間とかではなくDDLを計算して出す時間がほとんど
  • 本番でやべってなって、手でインデックスを貼ったりすると、GitDDLが知っているDDLとの差分が出てしまって戻すのに手こずる

それで?

で、ここまで書いてアレなんですが、OSS紹介 Advent Calendarの5日目に書かれた、schemalexが思想を引き継いだ上で欠点が解消されている(と思われる)ので、今僕の中で移行計画を練ってます。とはいえ、GitDDLはかなり実績のあるものですし、あれだけ多いテーブル数で変なDDLを吐くとか(カラムのリネームはちょっと厳しいんですけれど)は無いので、これからもある程度の規模のアプリには使えると思われます。

参考