FluentMigration の基本機能メモ

前回簡単に使ってみた FluentMigration の機能を整理できたのでメモがてら。ほとんど公式ドキュメントの翻訳みたいになっちゃってますが。

そもそも FluentMigrator って何って話は前回のエントリをどぞ。

kendik.hatenablog.com

なお、ほとんどの例は公式から持ってきています。

目次

Package Install

Install-Package FluentMigrator

もしくは

Install-Package FluentMigrator.Tools

違いは Runner の有無。Tools の方は Runner が付いてきます。
Tools は NuGet パッケージではあるんですが、プロジェクトの参照設定に入ったりするわけではなく、自分でパッケージを展開して中の Runner をコマンドプロンプトなどから利用します。
ツール群を zip で落としたと思えばわかりやすいですかね。

Runner

以上四つが公式ランナーです。Rake ランナーは Albacore が必須です。

Migration

一回のマイグレーションのために、Migration 抽象クラスを実装したクラスを一つ作ります。
再掲になりますが、基本的には以下のような実装を自分で行えばいいです。

[Migration(1)]
public class SampleMigration : Migration
{
    public override void Up()
    {
        // Forward Migration の実装
    }

    public override void Down()
    {
        // Backward Migration の実装
    }
}

MigrationAttribute の付与と、Migration 抽象クラスの継承ですね。
MigrationAttribute の引数には long 型のバージョン番号を渡します。(後述しますが、カスタマイズ可能)

また、初回のマイグレーション時には自動で管理用テーブルを作成します。あまり気にする必要はないと思いますが、テーブル定義は以下の通りです。PostgreSQL 版なので、他の DBMS を使っている場合は適当に読み替えてください。

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;

このテーブルの管理を FluentMigrator に任せておけば同じマイグレーションは重複して実行されません。逆に、なんでもいいからまっさらに戻したいときは drop table すれば OK です。

Profile

マイグレーションクラスにプロファイルを設定できます。

[Profile("Dev")]
public class SampleMigration : Migration
{
    public override void Up()
    {
        // Forward Migration の実装
    }

    public override void Down()
    {
        // Backward Migration の実装
    }
}

Runner のオプションでプロファイルを指定可能なのですが、それが一致したときのみ実行されるマイグレーションの定義が可能です。開発環境、テスト環境、本番環境それぞれで処理を分けたいときなどに使えますね。注意点として、プロファイルが設定されたマイグレーションは常に実行されるようです。
開発環境やテスト環境でテストデータを整理するのに使って欲しいらしいです。

Multiple Database

IfDatabase("oracle").Execute.Script("CreateViewsOracleMigrationUp.sql");
IfDatabase("sqlserver").Execute.Script("CreateViewsSqlServerMigrationUp.sql");

複数DBMS に対応するマイグレーションクラスを作成できます。

DB Function

Create.Table("TestTable").WithColumn("Created").AsDateTime().Nullable().WithDefault(SystemMethods.CurrentDateTime);

「最低限これは使うだろ」というような DB 関数は直接 SQL を書かなくても実行可能です。
列のデフォルト値などに使う用途を想定しているみたいですね。
サポートしている関数は各 DBMS の実装に依存しますが、インタフェースとして用意されているのは

  • NewGuid
  • NewSequentialId
  • CurrentDateTime
  • CurrentUTCDateTime
  • CurrentUser

の五つです。
例えばポスグレだと NewSequentialId 以外はサポートされています。
ただ、NewGuid を使うためには contrib の uuid-ossp 拡張のインストールと有効化が必要です。

Fluent Interface

名前に Fluent と付いているのはこいつがあるからでしょう。
Fluent Interface とは、マイグレーションクラスの実装時に使える SQLHelper とか QueryBuilder みたいな Utility クラス群を指します。
これらのクラスはその名の通り Fluent な API を持つので、わかりやすくかつ気持ちよくマイグレーションクラスを実装できます。

// 簡単な例
Create.Table("users")
    .WithColumn("user_id")
        .AsGuid()
        .PrimaryKey("pk_user")
        .WithDefault(SystemMethods.NewGuid)
    .WithColumn("user_name")
        .AsAnsiString(10)
        .NotNullable()
        .WithDefaultValue("名無しさん")
        .WithColumnDescription("コメントも書ける");

Fluent Interface の起点クラスはいくつかあるので順に紹介します。

Create Expression

上記例でも使っているクラスです。
テーブル、列、インデックス、外部キー、シーケンス、スキーマが作成可能です。
データベースは作れない ので注意が必要です。

Alter Expression

Alter.Table("Bar")
    .AddColumn("SomeDate")
    .AsDateTime()
    .Nullable();

Alter.Table("Bar")
    .AddColumn("SomeDate")
    .AsDateTime()
    .SetExistingRowsTo(DateTime.Today)
    .NotNullable();

テーブル、列に対して定義の変更が可能です。

Delete Expression

Delete.Table("Users");

Delete.FromTable("Users").AllRows(); // delete all rows
Delete.FromTable("Users").Row(new { FirstName = "John" }); // delete all rows with FirstName==John
Delete.FromTable("Users").IsNull("Username"); //Delete all rows where Username is null

テーブル、列、インデックス、外部キー、制約とスキーマの削除が可能です。
また、テーブルを指定したあとにメソッドチェーンを続けることで行の削除も行えます。対象の抽出条件 (where 句)も指定可能です。

ただ、抽出条件には制限があって、基本的に「equal」での比較しか行えません(is null 含む)。つまり、「not equal」や「lower than equal」などは使えません。

Execute Expression

Execute.Script("myscript.sql");
Execute.EmbeddedScript("UpdateLegacySP.sql");
Execute.Sql("DELETE TABLE Users");

SQL スクリプトか、SQL スクリプトを書いた外部ファイルを実行できます。

外部ファイルの実行は、そのファイルがマイグレーションクラスが属するプロジェクトに追加されていて、かつそのファイルの [プロパティ] > [ビルドアクション] が「埋め込まれたリソース」になっている必要があります。

Fluent Interface を QueryBuilder として見てしまうとどうしても弱く (特に検索)、時にはやりたいことができないことも出てくると思うのでそういう状況下で利用します。SQL Server だったら T-SQL、ポスグレだったら無名ブロックを使えばだいたい何でもできるので、利用して困っても大抵何とかなるはずです。

Rename Expression

Rename.Table("Users").To("UsersNew");

テーブルか列のリネームが行えます。

Data Expression

Insert.IntoTable("Users").Row(new { FirstName = "John", LastName = "Smith" });

// delete は前述のため省略

Update.Table("Users").Set(new { Name = "John" }).Where(new { Name = "Johnanna" });

行の挿入、更新、削除が行えます。
匿名型を列の値として渡すことが可能です。Dapper っぽいですね。

IfDatabase Expression

IfDatabase("SqlServer", "Postgres")
    .Create.Table("Users")
    .WithIdColumn()
    .WithColumn("Name").AsString().NotNullable();

IfDatabase("Sqlite")
    .Create.Table("Users")
    .WithColumn("Id").AsInt16().PrimaryKey()
    .WithColumn("Name").AsString().NotNullable();

複数 DBMS を使う際、任意の DBMS を指定した上で他の Expression を実行できます。

Schema.Exists Expression

if (!Schema.Table("Users").Column("FirstName").Exists())
{
    this.Create.Column("FirstName").OnTable("Users").AsAnsiString(128).Nullable();
}

テーブルや列の存在確認が行えます。

SQL Server only Expression

SQL Server 専用の API です。

  • クラスタ化インデックス、非クラスタ化インデックスの作成
  • Identity 列に値を設定
  • Identity 列のオプション変更
  • 明示的に Unicode 文字列を指定

Raw Sql Helper for inserting data

Insert.IntoTable("User").Row(new { Username = RawSql.Insert("CURRENT_USER") })

名前からちょっと誤解しそうですが、生で SQL がなんでも書ける機能ではないです。
Fluent Interface での行挿入時の値は string 型だと強制的に文字列リテラルとして扱われてしまうため、例えば CURRENT_USER 関数を使おうと

Insert.IntoTable("Users").Row(new { Username = "CURRENT_USER" });

こう書いてしまうと、"CURRENT_USER" というリテラルがセットされてしまいます。
そこで、冒頭の例のように RawSql クラスを使ってやると、うまいこと式に変換してくれます。

Auto Reversing Migration

マイグレーションクラスは通常 Migration 抽象クラスを継承し、Up() メソッドと Down() メソッドの両方の実装が必要です。
ですが、AutoReversingMigration 抽象クラスを継承すれば Down() メソッドを、Up() メソッドの実装から推測して自動で作成してくれます。

ただまぁ制限があって、サポートしている Fluent Interface は

  • Create.Table
  • Create.Column
  • Create.Index
  • Create.ForeignKey
  • Create.Schema
  • Delete.ForeignKey
  • Rename.Table
  • Rename.Column

だけです。
まぁ単調な Backward 処理を毎回書くよりはマシですかね。。。

Two Transaction Mode

マイグレーションを実行する際のトランザクションは FluentMigrator が自動で行うのですが、トランザクションのスコープを変更することができます。

  • None
  • Transaction-Per-Migration (既定値)
  • Transaction-Per-Session

上二つは MigrationAttribute 属性への引数で指定可能なのですが、Transaction-Per-Session を有効にするには Runner にオプション指定が必要です。

Application Context

Runner に値を渡すか、マイグレーションクラス中で Context を設定できます。

値の設定

migrate.exe ... --context MyArgument
var migrationContext = new FluentMigrator.Runner.Initialization.RunnerContext(announcer)
{
    ApplicationContext = clientName
};

値の取得

if ((string)this.ApplicationContext == "MyArgument")
    this.Delete.Column("BadColumn").FromTable("MyTable");

ApplicationContextobject 型なので、マイグレーションクラス中での設定であれば任意の型を渡せます。
Runner からの場合は型はどうしようもないですが、文字列として受けてスイッチに使って欲しいみたいですね。JSON とか突っ込んでコード中でデシリアライズすれば何とかなるかもしれませんw

Tag

[Tags("DK", "NL", "UK")]
[Tags("Staging", "Production")]
[Migration(1)]
public class DoSomeStuffToEuropeanStagingAndProdDbs() { }

マイグレーションクラスにタグを設定できます。
タグは Runner にオプションとして指定することで、任意のマイグレーションのみを実行できます。

Original Format Migration Version

機能というか拡張ポイントなのですが、、、MigrationAttribute に渡すバージョン番号に独自ルールを設けることができます。

public class MyCustomMigrationAttribute : FluentMigrator.MigrationAttribute
{
    public MyCustomMigrationAttribute(int branchNumber, int year, int month, int day, int hour, int minute, string author)
       : base(CalculateValue(branchNumber, year, month, day, hour, minute))
   {
       this.Author = author;
   }
   public string Author { get; private set; }
   private static long CalculateValue(int branchNumber, int year, int month, int day, int hour, int minute)
   {
      return branchNumber * 1000000000000L + year * 100000000L + month * 1000000L + day * 10000L + hour * 100L + minute;
   }
}

[MyCustomMigration(author: "Scott Stafford", branchNumber: 12, year: 2012, month: 8, day: 7, hour: 14, minute: 01)]
public class TestLcmpMigration : Migration
{
    public override void Down() { /* ... */ }
    public override void Up() { /* ... */ }
}

標準設定だと数値しか使えませんが、拡張することで実行者や実行日時も指定できるのはいいですね。

Custom VersionInfo Table

これも拡張ポイントですが、自動で作成される VersionInfo テーブルはカスタマイズ可能です。
IVersionTableMetaData インタフェースを実装するクラスを定義し、VersionTableMetaDataAttribute 属性を付与することで VersionInfo テーブル定義を自由にできます。

また、一部だけ拡張できればいいなら DefaultVersionTableMetaData クラスを継承します。

Contrib

現存する DB からリバースエンジニアリングというか、スキーマダンプできます。
「SchemaDump」と「FluentMigrator.T4」という二つのライブラリがあります。

ドキュメントがなくて詳細はわからないのですが、後者は T4 とかついているもののどちらも初期化マイグレーションクラスを作るための Helper に見えます。。。

使い方は、どちらも既存 DB のスキーマ定義を持ったオブジェクトを自動で作ってくれるので、それをもとにして動的に .cs ファイルを頑張って作成するようです。

GitHub 上でも突っ込み入ってますが、FluentMigrator アセンブリ中に存在しないので利用するにはソースコード持ってこないといけません。

色々イマイチだしドキュメントもないから、もしかして作りかけなのかな。。。