読者です 読者をやめる 読者になる 読者になる

DB マイグレーションツールの Fluentmigrator がいい感じかも

仕事で使える DB マイグレーションツールが欲しくてですね。

C# だと Entity Framework が鉄板なのかもしれないですが、今の仕事では使ってないのでなんか別で考えないといけなくて探してみたところ、 FluentMigrator というライブラリを見つけたので、ちょっと動かしてみました。

github.com

FluentMigrator とは

.NET 環境で使える DB マイグレーション用ライブラリです。

Ruby on Rails の rake db を意識しているみたいですね。試してないですが、作成したマイグレーションファイルは Albacore *1 を使って rake でも実行できるみたいです。

公式のドキュメントがすごく丁寧で、ちょっとまだ読んでる最中で詳細は別の機会にエントリあげようかと思いますが、割と十分な機能を備えているように見えます。とりあえず基本的な Forward, Backward の実行や環境別設定ができるのは確認しました。

サポート対象 DB も多くて、SQL Server, Oracle, MySQL, PostgreSQL, Jet, SQLite で利用可能です。.NET 系ライブラリって SQL Server しかサポートしてないライブラリもちょいちょいあるので、この時点で結構期待を持てますねー。

前提

早速使ってみますが、今回 DB は仕事でも使ってる PostgreSQL 9.4 を使うことにします。
まぁ多分 SQL Server も同じ実装で動くんじゃないでしょうか。試してませんが、FluentMigrator は複数の DB をサポートしてるのでインタフェースも抽象化されてると思いますしきっと大丈夫!

で、前準備として PostgreSQL に DB は作っておきます*2
また、VS のプロジェクトテンプレートはクラスライブラリを選択します (後述)。

インストール

インスト-ルは NuGet からです。

基本 FluentMigrator と FluentMigrator.Tools のふたつのパッケージがあって、マイグレーションファイルの作成自体は前者だけでいいんですが、それのランナーが後者に含まれる感じになります。
ランナーは必要ですし、FluentMigrator.Tools は FluentMigrator に依存しているので基本後者だけインストールすれば OK です。

PM> Install-Package FluentMigrator.Tools

参照設定に FluentMigrator.Tools が入ってこなくて不安になりますが、多分こういう仕様なんでしょう。気にせずいきます。

マイグレーションファイルの作成と実行

基本構造

マイグレーションファイルの基本的な構造は以下の通りです。

[Migration(1)]
public class SampleMigration : Migration
{
    public override void Up()
    {
        throw new NotImplementedException();
    }

    public override void Down()
    {
        throw new NotImplementedException();
    }
}

基本ルールは

  1. Migration 抽象クラスを継承する
  2. そのクラスに Migraion 属性を付与する

だけです。

Migration 属性の引数はマイグレーションの識別子です。Forward/Backward の順序を決めるので、数値の大小には気をつける必要があります。日付を入れたい場合は 20150630 という数値として扱えば大丈夫です。

Up() メソッドには Forward の実装、Down() メソッドには Backward の実装を書きます。

FluentAPI

ファイルができたらメソッドを実装していくわけですが、そこで利用するヘルパーのことを Fluent API と呼んでいるようです。なんでそう呼ばれるかは実装見たらすぐわかると思うので以下サンプルをどうぞ。

[Migration(1)]
public class SampleMigration : Migration
{
    public override void Up()
    {
        Create.Table("users")
            .WithColumn("user_id")
                .AsGuid()
                .PrimaryKey("pk_user")
                .WithDefault(SystemMethods.NewGuid)
            .WithColumn("user_name")
                .AsAnsiString(10)
                .NotNullable()
                .WithDefaultValue("名無しさん")
                .WithColumnDescription("コメントも書ける")
            .WithColumn("age")
                .AsInt32()
                .NotNullable()
            .WithColumn("hobby")
                .AsString()
                .Nullable()
            .WithColumn("update_timestamp")
                .AsDateTimeOffset()
                .NotNullable()
                .WithDefault(SystemMethods.CurrentDateTime);
    }

    public override void Down()
    {
        Delete.Table("users");
    }
}

何やってるかわかりやすくていいですね。
ここでは以下の定義を持つ users テーブルを作ってます。

create table users
(
    user_id uuid not null default uuid_generate_v4()
    , user_name varchar(10) not null default '名無しさん' -- コメントも書ける
    , age integer not null
    , hobby text
    , update_timestamp timestamp with time zone not null default current_timestamp

    constraint pk_users praimary key (user_id)
);

ちなみに今回はやってないですが、もちろん外部キーなんかも設定できますよ。

Down() メソッドはテーブルの drop をやってます。
Delete ってクラス名はどうなのかな、、、と思わなくもないですが、このクラスは行の削除インタフェースも持っているので drop だとちょっと違和感あるし、仕方ないのかもしれません。

もうひとつマイグレーションクラスを作ってみます。今度はデータの挿入です。

[Migration(2)]
public class SampleMigration2 : Migration
{
    public override void Up()
    {
        Insert.IntoTable("users")
            .Row(new
            {
                user_name = "maekawa",
                age = 15,
                hobby = "猫カフェ巡り",
            })
            .Row(new
            {
                user_name = "shiburin",
                age = 15,
                hobby = "犬の散歩"
            });
    }

    public override void Down()
    {
        // すべての行を削除するはずだが、実行するとエラー。バグか?
        //Delete.FromTable("users").AllRows(); 

        Delete.FromTable("users").Row(new { age = 15 });
    }
}

データの登録、変更、削除を行うにはテーブル名を指定したあと、Row() メソッドで値を設定します。
匿名クラスを使えるのはいいですねー。

これらのマイグレーションクラスは作成したらビルドして、dll を作成しておきましょう。

マイグレーションの実行

マイグレーションは、マイグレーションクラスを含んだアセンブリを Migration Runner に渡すことで実行します。最初にプロジェクトテンプレートをクラスライブラリにしたのはそういう訳ですね。
(別に exe もアセンブリですし、Runner も多分さっき作ったマイグレーションクラスを叩いているだけだと思うのでコンソールアプリケーションで作っても実は何の問題もないと思います)

Migration Runner は、

  • Command Line Runner
  • NAnt Runner
  • MSBuild Runner
  • Rake Runner

の四つがあるようです。
今回は Command Line Runner を使うことにします。

Runner は、FluentMigration.Tools パッケージの中に含まれており、FluentMigration.Tools の中身はインストール時にどこかに自動展開される訳ではないようなので、自分で NuGet パッケージキャッシュフォルダを開いて .nupkg を展開してください*3

展開した結果の Migrate.exe が今回利用する Command Line Runner です。

f:id:kendik:20150630201704p:plain

exe はいくつかの同梱されているアセンブリに依存しているので、フォルダごとどこか適当なところにコピーし、コマンドプロンプトPowerShell Console から実行します。

.\Migrate.exe /connection "Server=<ip>; Port=5432; Database=postgres; User Id=postgres; Encoding=UNICODE;" /db postgres /target <マイグレーションアセンブリのファイルパス>

f:id:kendik:20150630201725p:plain

こんな感じで結果が表示されます。
Runner に --verbose=true オプションを付けると実行された SQL や実行時間も見ることができます。

f:id:kendik:20150630201734p:plain

便利ですね。
あとは Dry Run があると嬉しいところなのですが、オプションにないので対応してなさそうです。

作成された users テーブルの、実際の定義を抜粋します。

CREATE TABLE users
(
  user_id uuid NOT NULL DEFAULT uuid_generate_v4(),
  user_name character varying(10) NOT NULL DEFAULT '名無しさん'::character varying, -- AsString() にすると text 型になるよ
  age integer NOT NULL,
  hobby text,
  update_timestamp timestamp with time zone NOT NULL DEFAULT now(),
  CONSTRAINT pk_user PRIMARY KEY (user_id)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE users
  OWNER TO postgres;
COMMENT ON COLUMN users.user_name IS 'AsString() にすると text 型になるよ';

期待通りですね。素晴らしい。

Backward したいときは、Runner に --task rollback オプションです。

.\Migrate.exe /connection "Server=<ip>; Port=5432; Database=postgres; User Id=postgres; Encoding=UNICODE;" /db postgres /target <マイグレーションアセンブリのファイルパス> --task rollback

f:id:kendik:20150630201744p:plain

注意点

VersionInfo テーブル

先ほどの実行結果の画像で気がついた方もいるかもしれませんが、FluentMigrator は VersionInfo というテーブルを自動で作成します。定義はこんな感じです。

CREATE TABLE "VersionInfo"
(
  "Version" bigint NOT NULL,
  "AppliedOn" timestamp without time zone,
  "Description" character varying(1024)
)
WITH (
  OIDS=FALSE
);
ALTER TABLE "VersionInfo"
  OWNER TO postgres;

マイグレーションの実行履歴を管理してくれるテーブルで、それ自体は別にどうということもないですね。
が、ポスグレだとダブルクォートもテーブル名に含まれるので注意が必要です。ポスグレは大文字を使いたいときはダブルクォートで括らないといけないんですが、それがオブジェクト名に含まれてしまうという残念仕様なので、このテーブルにクエリを投げたいときは select * from "VersionInfo"; としないといけません。

ちなみに VersionInfo テーブルは、最後まで Backward するとちゃんと自動で削除してくれます ;)

一部のデータ型がサポートされていない

ポスグレ限定の話ですが、例えば列挙型や配列、JSON 型なんかは現在サポートされていません。これはポスグレ使いにはかなり注意が必要ですね。
利用可能なポスグレの型と C# 上の型マッピング実装をご確認ください。

調べてませんが、多分他の DBMS でも未サポートの型がありそうな気がします。

QueryBuilder は貧弱

データ登録マイグレーションに使った QueryBuilder ですが、結構貧弱です。

まだちょっと触っただけですが、Not Equal が使えない、(Greater|Lower) than が使えない、is not null が使えない、Select-Insert できない、Create Table As が使えない、などなど。
QueryBuilder ライブラリでないことはわかっていますが、QueryBuilder として作られた他のライブラリと比較するとだいぶ残念です。

ただ、SQL をファイルにしてそれを実行したり、コード上に SQL スクリプトを書くことはできるのでそちらでとりあえずカバーは可能です。

ポスグレの話をすると、Execute.Sql() メソッド無名コードブロックの実行が可能なので正直どうとでもなります。ご安心を。

まとめ

非常に簡単に FluentMigrator を触ってみましたが、結構いけてません?もうちょい調べて、問題なければ仕事に投入したいですね。

あと、今回試したもの以外にも Fluent API はもっと豊富ですし他にも面白そうな機能があるんですがそれはまた後日。

公式ドキュメントが非常に詳しいので、気になる方は是非そちらを。

*1:https://github.com/Albacore/albacore

*2:FluentMigrator、DB 作れないように見えるんですが、、、まさかそんな。。。

*3:インストール時に展開してプロジェクトのアイテムとして含めちゃっていいと思うんですけどねー