Fluent でカスタムマイグレーションを作る方法

これは「[Vapor Advent Calendar 2018](https://qiita.com/advent-calendar/2018/vapor)」9日目の投稿です。

## はじめに

[前回の投稿](https://qiita.com/5t111111/items/572196e38df88646c847)では、「本番運用するアプリでモデルの自動マイグレーションを使ってはいけない」ということと、その代わりに公式ドキュメントも推奨するカスタムマイグレーションを使うべきということを書きました。

今回は、その「カスタムマイグレーション」の具体的な作り方を書いていきたいと思います。

## スキーマを作成するカスタムマイグレーションを作成する

モデルの自動マイグレーションはテーブルを `CREATE` (or `DROP`) しスキーマを作成するものでした。まず、それと同じ働きをするカスタムマイグレーションを作ってみましょう。

まず、以下の `Article` モデルがあるとします (簡略化のため、前回のモデルとはプロパティなど変更しています) 。

```Article.swift
import FluentMySQL

final class Article: MySQLModel {
    var id: Int?
    var title: String?
}
```

このとき、テーブルを `CREATE` するマイグレーション (つまりこの時点ではモデルの自動マイグレーションと同等のもの) をカスタムマイグレーションとして作ると以下のようになります。

```CreateArticle.swift
import FluentMySQL

struct CreateArticle: MySQLMigration {
    static func prepare(on conn: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.create(Article.self, on: conn) { builder in
            builder.field(for: \.id, isIdentifier: true)
            builder.field(for: \.title)
        }
    }

    static func revert(on connection: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.delete(Article.self, on: connection)
    }
}
```

モデルの自動マイグレーションの場合は `Migration` プロトコルに適合させましたが、カスタムマイグレーションでは `<データベース名>Migration` プロトコルを利用します。もちろん、上記で `MySQL*` となっている箇所は別のデータベースを利用する場合はそれに応じて変更されます。

サンプルコードに書いたように、実装すべきメソッドは `prepare` と `revert` の2つです。知らなくても想像がつくと思いますが、マイグレーションを実行した際に `prepare` が実行され、revert するときに `revert` が実行されます。また、データベース上のデータ型は、自動マイグレーションと同様にモデルのプロパティから自動的に決定されます (指定することも可能です) 。実装に関しての詳細はドキュメントなどを参照してください。

モデルの自動マイグレーションと同様に、実行したいマイグレーションは登録しておく必要があります。

```configure.swift
...

var migrations = MigrationConfig()
migrations.add(migration: CreateArticle.self, database: .mysql)
services.register(migrations)

...
```

ポイントは、`add` メソッドの引数で `migration` を指定することです。これで、`vapor run` などを実行したときにカスタムマイグレーションが実行されるようになります。

このようにカスタムマイグレーションとして実装することで、マイグレーションをモデルの設計に暗黙的に依存するのではなく、独立して明示的に扱えるようになります。

## スキーマを更新するカスタムマイグレーション

モデルの自動マイグレーションには、テーブルを `ALTER` しスキーマを更新することができないという制限があります。カスタムマイグレーションを使うとスキーマの更新が可能です。

先ほどの `Article` モデルに `article_url` というカラムを追加するカスタムマイグレーションは以下のように書きます。

```Article.swift
import FluentMySQL

final class Article: MySQLModel {
    var id: Int?
    var title: String?
    var article_url: String? // 追加
}
```

```AddArticleArticleId.swift
import FluentMySQL

struct AddArticleArticleId: MySQLMigration {
    static func prepare(on conn: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.update(Article.self, on: conn) { builder in
            builder.field(for: \.article_id)
        }
    }

    static func revert(on conn: MySQLConnection) -> Future<Void> {
        return MySQLDatabase.update(Article.self, on: conn) { builder in
            builder.deleteField(for: \.article_id)
        }
    }
}
```

データベース操作の API が `create` から `update` などと変わっただけで、全体の作りとしてはスキーマを作成するカスタムマイグレーションと同様ですね。

当然、このマイグレーションも登録しておく必要があります。

```configure.swift
...

var migrations = MigrationConfig()
migrations.add(migration: CreateArticle.self, database: .mysql)
migrations.add(migration: AddArticleArticleId, database: .mysql)
services.register(migrations)

...
```

これでスキーマ更新もマイグレーションとして管理することが可能になります。

## まとめ

モデルのカスタムマイグレーションは手軽で便利です。実際、開発中は積極的に活用していくべきでしょう。開発中であればモデルの設計も頻繁に変わるでしょうし、重要なデータが入っているわけでもないでしょうから、スキーマが変わったら単純にデータベースを作り直してしまえばよいので。

しかし、本番運用となるとそうはいきませんので、実際にリリースするアプリケーションでは、ここで挙げたようなカスタムマイグレーションを利用するようにするのが良いと考えています。

Vapor のコア開発者である Tanner も[同様の発言](https://github.com/vapor/fluent/issues/577)をしています。

> you implement separate migrations from the get-go. Being explicit is very advantageous IMO.

Ruby on Rails などの他のメジャー Web Application Framework を使っていた人なら、明示的にマイグレーションファイルを用意する方が自然に感じるでしょうし、ケースバイケースで効率的に使い分けていければよいですね。