FluentMigratorでデータベースマイグレーションを行う(dotnet)

# はじめに

まず、ここでいうデータベースマイグレーションとは、アプリケーションで使うためのデータベーススキーマとデータを
作成するという意味(Ruby on Railsのアレ)としておく。

# dotnet系プロジェクトにおけるデータベースマイグレーション

dotnet系プロジェクトでのデータベースマイグレーションというと、真っ先に思いつくのが[Entity Framework Core(EFCore)](https://docs.microsoft.com/en-us/ef/core/)によるものだろう。
実際多くの場合で、EFCoreを使えば問題は無い。
だが、筆者の場合、クエリはほぼDapper経由で出しているため、クエリビルダ等は必要なく、欲しいものはマイグレーションのみとなる。
マイグレーションのみの用途となると、EFCoreは大がかりに見えて、個人的にはちょっと扱いにくいなと思った。

そこで、よりマイグレーションのみに特化したライブラリは無いものかと探して、[FluentMigrator](https://fluentmigrator.github.io/)を見つけた。
自分にとっては丁度良い規模感だったため、今回の記事で紹介する。

# FluentMigratorとは

先にも述べた通り、C#でデータベースマイグレーションを行うためのライブラリとなる。特徴としては、

* バージョン番号によるスキーマ管理
    * バージョン番号は64bit整数で自分で管理する
* C#でマイグレーション記述
    * SQL直接発行も可能
* 複数RDBMSに対応可能
    * 特化処理を書くことも可能
* 各RDBのSQL実行ランタイムを必要としない
    * SQLServerにおけるsqlcmd、PostgreSQLにおけるpsql等
    * dotnetランタイムは必要
        * publishすることで制約の緩和は可能

となる。

# 使用方法

最初のプロジェクトを作成して、実際にデータベースに適用するまでの流れを書く。なお、FluentMigratorは、記事執筆時点での最新版である5.1.0を基準に書く。
FluentMigratorはメジャーバージョンごとの変化が割と大きいので、注意すること。

大きく分けると、

* マイグレーションの作成
* マイグレーション実行アプリの作成

に分けられる。

## マイグレーションプロジェクトの作成

まず、どのようにデータベースを作成するか(テーブル定義等)、というプロジェクトを作成する。
といっても、中身は通常のC#クラスライブラリプロジェクトである。

1. `dotnet new classlib --name [プロジェクト名]` で雛形を作成
2. `FluentMigrator` NuGetパッケージを追加
    * 各RDBに対する参照は、後述のマイグレーション実行プロジェクトで追加するので必要ない
3. マイグレーションクラスを作成

マイグレーションクラスは以下のように作る。

```csharp
using System;
using FluentMigrator;

namespace fmlib1
{
    // バージョン番号をプロジェクトで一意にする
    // 重複があるとエラーになる
    // 2回目以降は、同じ番号のマイグレーションはスキップする
    // カスタム属性を使うとより管理しやすい
    // https://fluentmigrator.github.io/articles/migration/migration-attribute-custom.html
    [Migration(0, "creating schema")]
    public class CreateSchema : Migration
    {
        public override void Down()
        {
            // バージョン番号を下げるときに実行される
            Delete.Table("testtable").InSchema("aa");
            Delete.Schema("aa");
        }

        public override void Up()
        {
            // バージョン番号を上げるときに実行される
            Create.Schema("aa");
            Create.Table("testtable").InSchema("aa");
        }
    }
}
```

ポイントは、

* publicクラスにする
* `FluentMigrator.MigrationAttribute`をクラスに付与する
    * **一意になるバージョン番号を付ける**
        * 小さい値のものから実行される
        * 管理が大変という場合は、[カスタム属性で一定の規則性を持たせるという手段がある](https://fluentmigrator.github.io/articles/migration/migration-attribute-custom.html)
* `FluentMigrator.Migration`クラスを継承する
    * `FluentMigrator.IMigration`でも認識されるが、実用上はこちらを使う方が良い

で、どれかが欠けると認識されなくなるので注意。
各マイグレーションは番号で識別され、同じ番号のものは二度実行されないようになっている。

### RDB種別ごとに特殊な処理を実行する

FluentMigratorでは、マイグレーション記述においては各RDBMSの違いを吸収できるように作られているが、ある特定のRDBの場合のみ特殊な処理をしたい(ストアドを登録したい等)時は、以下のようにする。

```csharp
// SQLServer系列の場合だけ、デフォルトの文字列照合設定を変更する
IfDatabase("SqlServer")
  .Execute.Sql("alter database CURRENT COLLATE Japanese_BIN");
```

ここで`IfDatabase()`の引数に何を指定できるかというと、[公式ドキュメントのサポートデータベース一覧](https://fluentmigrator.github.io/)の"Identifier"、または"Alternative identifier"が使える。

### Nullableに関する注意点

テーブル列を作成する時、null許可を指定するには以下のようにする

```csharp
// null許可する場合
Create.Column("col1").OnTable("table1").AsString(128).Nullable();
// null許可しない場合
Create.Column("col2").OnTable("table1").AsString(128).NotNullable();
```

ここで、 **明示的にnull許可を指定しない場合、not null指定がついてしまう** ので、必ずnull許可指定は入れること。

## FluentMigrator公式のマイグレーション実行アプリを使用する

とりあえず試してみたい場合、FluentMigrator公式のコンソールアプリが存在するので、そちらを使用する。
3.2.1時点では、[FluentMigrator.Console](https://www.nuget.org/packages/FluentMigrator.Console/)と[FluentMigrator.DotNet.Cli(dotnet-fm)](https://www.nuget.org/packages/FluentMigrator.DotNet.Cli/)の2種類が存在している。
前者が`nuget.exe install`でダウンロードして実行する用で、後者がdotnet global toolとなる。

詳しいオプションはそれぞれ公式ドキュメントが存在するのでそちらを参照。

* [FluentMigrator.Console](https://fluentmigrator.github.io/articles/runners/runner-console.html)
* [FluentMigrator.DotNet.Cli](https://fluentmigrator.github.io/articles/runners/dotnet-fm.html)

ただ、接続文字列の指定の方法など、不便な所も多いため、公式では自分で実行アプリを作成するのが推奨されている。

## マイグレーション実行プロジェクトを作成する

さて、前段では、どのようにマイグレーションが実行されるか定義しただけで、これ単体で実行ができるわけではない。
記述した情報に基づいて、実際にマイグレーションを実行するためのアプリケーションを作成する。

### パッケージの準備

中身としては、通常のdotnetアプリとなるが、以下の参照を追加する。

* FluentMigrator.Runner.Core
    * [FluentMigrator.Runner](https://www.nuget.org/packages/FluentMigrator.Runner) を追加すればとりあえず全部入りとなる
* FluentMigrator.Runner.[対応するRDBMS]
* RDBに対応するアセンブリを含むNuGetパッケージ(Npgsql、Microsoft.Data.SqlClient等)
    * 別途追加が必要なのは、`FluentMigrator.Runner.*`パッケージ自体では型を直接参照しておらず、リフレクションでADO.NETのDbProviderFactory派生のクラスを取得しているため
    * SQLiteであればMicrosoft.Data.Sqlite等
    * 対応アセンブリは後述
* Microsoft.Extensions.DependencyInjection
    * 設定などはこのライブラリ前提で設定を行うため
* マイグレーションプロジェクトライブラリ
    * 実行時に`typeof(T)`からAssemblyを簡単に引けるようにするため
    * 決定できない場合は、`System.Reflection.Assembly.LoadFrom()`等で`Assembly`を取得してもOK

対応アセンブリは以下のようになる。

| RDB | 対応アセンブリ(どれか一つあればOK) |
|-----|-----------------|
| SQLServer | System.Data.SqlClient<br/>[Microsoft.Data.SqlClient](https://github.com/fluentmigrator/fluentmigrator/issues/1023) |
| PostgreSQL | Npgsql |
| SQLite | System.Data.SQLite<br/>Mono.Data.Sqlite<br/>Microsoft.Data.Sqlite |
| DB2 | IBM.Data.DB2.Core<br/>IBM.Data.DB2 |
| Firebird | FirebirdSql.Data.FirebirdClient |
| SAP Hana | Sap.Data.Hana<br/>Sap.Data.Hana.v4.5 |
| MySQL | MySql.Data |
| dotConnect for Oracle | DevArt.Data.Oracle |
| Redshift | Npgsql |
| ~~SAP SQL Anywhere~~ | ~~Sap.Data.SQLAnywhere.v4.5 <br/> Sap.Data.SQLAnywhere.EF6 <br/> iAnywhere.Data.SQLAnywhere.v4.5 <br/> iAnywhere.Data.SQLAnywhere.EF6~~<br/>5.1.0時点では廃止|
| ~~SQL Server CE~~ | ~~System.Data.SqlServerCe~~<br/>5.1.0時点では廃止 |
| Oracle | Oracle.DataAccess |
| Oracle(Managed) | Oracle.ManagedDataAccess |

[サポートしているDBの一覧](https://fluentmigrator.github.io/articles/multi-db-support.html)

### マイグレーションの実行コード実装

プロジェクトの設定が終わったら、実際にコードを書く。
大まかな手順としては、

1. 対象となるデータベースを作成
    * SQLite等、一部のRDBでは必要ない
2. `Microsoft.Extensions.DependencyInjection.ServiceCollection`のインスタンス作成
3. `AddFluentMigratorCore(this IServiceCollection)`を実行
4. `ConfigureRunner(this IServiceCollection, Action<IMigrationRunnerBuilder> configure)`で各種設定
    * `Add*`(サポートDBの追加)、`ScanIn`(マイグレーションプロジェクトのアセンブリを追加)、`WithGlobalConnectionString`(接続情報の設定)辺りをしておく
5. `Configure<SelectingProcessorAccessorOptions>(this IServiceCollection, Action<SelectingProcessorAccessorOptions>)`で、どのRDBに対して実行するか決定
    * ProcessorIdの設定が必要だが、オフィシャル提供アセンブリの有効値ならば`public static class FluentMigrator.ProcessorId`で取得可能
6. 設定が終わったら`BuildServiceProvider()`でIServiceProviderを作成
7. `IMigrationRunner`をサービスプロバイダから作成
8. 各種マイグレーションの実行

具体的なコードは[この辺り(github)](https://github.com/itn3000/testfluentmigrator/blob/6b46a7d43f9d4b9b0dad3a4104a3eea2c9c5d577/fmapp1/Program.cs#L34-L50)。
[また、公式ドキュメントにもサンプルコードがある。](https://fluentmigrator.github.io/articles/quickstart.html?tabs=runner-in-process)

## マイグレーション時のパラメーター

マイグレーション時に、何らかのパラメーターに基づいて動作させたい場合は、DIの仕組みを使うことになる。
マイグレーションの性質上、頻繁に使用する機能でもないが、それでも必要な場所では役に立つと思う。
具体的には以下のように行う

### マイグレーション側の準備

まず、パラメーターのインターフェイスを定義する

```csharp
public interface IMigrationParam
{
  int Data { get; }
}
```

次に、マイグレーション側で、コンストラクタにインターフェイスを受け取る処理を追加する

```csharp
[Migration(1, "")]
public class Migration1 : Migration
{
  IMigrationParam _Param;
  public Migration1()
  {
    // DIを設定していない場合にはこちらが呼ばれる
    _Param = null;
  }
  public Migration1(IMigrationParam p)
  {
    _Param = p;
  }
  // 以下マイグレーションコード....
}
```

### マイグレーション実行アプリ側の準備

通常のDIと同じように、ServiceCollectionに対して設定を行う。

```csharp
class MigrationParam1 : IMigrationParam
{
  public int Data => 1;
}
// var services = new ServiceCollection();
services.AddSingleton<IMigrationParam>(new MigrationParam1());
```

以上を行うと、実行時にServiceCollectionで設定したインスタンスが、マイグレーション側のコンストラクタに渡されるので、
マイグレーション側は、それを元に動作を決定することが可能になる。

### その他の設定

基本的な使い方については以上になるが、FluentMigratorは実は結構細かいところまで設定出来たりする。
ほとんどはあまり調整することのないパラメーターではあるが、知っておいて損は無い。
内容的にボリュームがあるので割愛するが、 [FluentMigrator公式コンソールアプリ](https://github.com/fluentmigrator/fluentmigrator/blob/6d9aaa79dedda882bd44c77dbb2e99d3cacf1f6f/src/FluentMigrator.Console/MigratorConsole.cs#L381-L464)が何をやっているのか見れば、参考になると思う。

# 終りに

今回紹介したFluentMigratorは、主流であるEntity Framework Coreを使用している場合にはほとんどの場合不要なものとなる。
しかし、結構細かいところまで制御が可能で、かつ規模感が動作を把握するのにちょうどいい感じなので、気に入った人は使ってみるのも良いかと思う。

今回使用した実験コード等は、 https://github.com/itn3000/testfluentmigrator に置いておいた。気が向いたら更新するかもしれない。