これは「Vapor Advent Calendar 2018」9日目の投稿です。
はじめに
前回の投稿では、「本番運用するアプリでモデルの自動マイグレーションを使ってはいけない」ということと、その代わりに公式ドキュメントも推奨するカスタムマイグレーションを使うべきということを書きました。
今回は、その「カスタムマイグレーション」の具体的な作り方を書いていきたいと思います。
スキーマを作成するカスタムマイグレーションを作成する
モデルの自動マイグレーションはテーブルを CREATE
(or DROP
) しスキーマを作成するものでした。まず、それと同じ働きをするカスタムマイグレーションを作ってみましょう。
まず、以下の Article
モデルがあるとします (簡略化のため、前回のモデルとはプロパティなど変更しています) 。
import FluentMySQL
final class Article: MySQLModel {
var id: Int?
var title: String?
}
このとき、テーブルを CREATE
するマイグレーション (つまりこの時点ではモデルの自動マイグレーションと同等のもの) をカスタムマイグレーションとして作ると以下のようになります。
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
が実行されます。また、データベース上のデータ型は、自動マイグレーションと同様にモデルのプロパティから自動的に決定されます (指定することも可能です) 。実装に関しての詳細はドキュメントなどを参照してください。
モデルの自動マイグレーションと同様に、実行したいマイグレーションは登録しておく必要があります。
...
var migrations = MigrationConfig()
migrations.add(migration: CreateArticle.self, database: .mysql)
services.register(migrations)
...
ポイントは、add
メソッドの引数で migration
を指定することです。これで、vapor run
などを実行したときにカスタムマイグレーションが実行されるようになります。
このようにカスタムマイグレーションとして実装することで、マイグレーションをモデルの設計に暗黙的に依存するのではなく、独立して明示的に扱えるようになります。
スキーマを更新するカスタムマイグレーション
モデルの自動マイグレーションには、テーブルを ALTER
しスキーマを更新することができないという制限があります。カスタムマイグレーションを使うとスキーマの更新が可能です。
先ほどの Article
モデルに article_url
というカラムを追加するカスタムマイグレーションは以下のように書きます。
import FluentMySQL
final class Article: MySQLModel {
var id: Int?
var title: String?
var article_url: String? // 追加
}
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
などと変わっただけで、全体の作りとしてはスキーマを作成するカスタムマイグレーションと同様ですね。
当然、このマイグレーションも登録しておく必要があります。
...
var migrations = MigrationConfig()
migrations.add(migration: CreateArticle.self, database: .mysql)
migrations.add(migration: AddArticleArticleId, database: .mysql)
services.register(migrations)
...
これでスキーマ更新もマイグレーションとして管理することが可能になります。
まとめ
モデルのカスタムマイグレーションは手軽で便利です。実際、開発中は積極的に活用していくべきでしょう。開発中であればモデルの設計も頻繁に変わるでしょうし、重要なデータが入っているわけでもないでしょうから、スキーマが変わったら単純にデータベースを作り直してしまえばよいので。
しかし、本番運用となるとそうはいきませんので、実際にリリースするアプリケーションでは、ここで挙げたようなカスタムマイグレーションを利用するようにするのが良いと考えています。
Vapor のコア開発者である Tanner も同様の発言をしています。
you implement separate migrations from the get-go. Being explicit is very advantageous IMO.
Ruby on Rails などの他のメジャー Web Application Framework を使っていた人なら、明示的にマイグレーションファイルを用意する方が自然に感じるでしょうし、ケースバイケースで効率的に使い分けていければよいですね。