Gradleを使ったWebアプリケーションのさくさく開発(マイグレーション編)

さくさく開発にかかせないのが、データベースのマイグレーション

今回は、Flywayを使って、GradleのタスクとしてFlywayのマイグレーションが実行できるようにします。FlywayはJava向けのDBマイグレーションフレームワークの一つで、個人的にはシンプルで扱いやすいかなと思って気に入ってます。機能面での比較は、Flywayのトップページの下の方にあります。強いてマイナス面を挙げるなら、ロールバック(=スキーマのダウングレード)ができない点くらいでしょうか(その辺の見解はFAQで言及されています)。

GradleとFlywayの連動を行うにはいくつか選択肢があります。素直にgradle-flyway-pluginを使ったり、GradleからAntタスクを呼び出す方法なども検討しましたが、最終的に、FlywayのコアAPIを使ってカスタムタスクを独自に作る方法を採用しました。

というわけで、ガイドの「第55章 カスタムタスクの作成」を参考に作っていきました。

追記(2014/2/19) 記事を書いた当時にはFlywayの最新バージョンは2.1でしたが、その後リリースされた2.2からはGradleがサポートされるようになりました(ちなみに2014.2.19時点での最新は2.3です)。そのため、このエントリで書いてある内容はすでに古くほとんど参考になりません。素直にオフィシャルのプラグインを使いましょう!

データベースの準備

PostgreSQL9.2をローカルで起動しておきます。また、利用するデータベース名は「flyway」とし、この名前の空のデータベースを作成しておきます。

設定ファイル

データベースの接続情報はビルドスクリプト内ではなく設定ファイルとして外出ししておきたいので、build.gradleと同じ階層に、gradle.propertiesを作り、設定情報を書いておきます。

jdbcDriver=org.postgresql.Driver
jdbcUrl=jdbc:postgresql://localhost:5432/flyway
jdbcUser=postgres
jdbcPassword=postgres

プロパティのキー名は何でもよいです(でも、jdbc.driver=org.postgresql.Driverのように、ドットでつながない方が良いかも)。この名前で直接ビルドスクリプト上からアクセスができます(参考:「14.2 Gradleプロパティとシステムプロパティ」)

マイグレーションSQLファイルの準備

src/main/resources/db/migrationディレクトリと、その下にSQLファイルを作っておきます。この辺はFlywayのデフォルトの構成に合わせたものです。

Flywayのサンプルにしたがって、まずはファイル名V1__Create_person_table.sqlとしてファイルを作ります。

create table PERSON (
    ID int not null,
    NAME varchar(100) not null
);

見ての通り、personテーブルを作るSQLです。

余談ですが、スキーマの更新内容はRubyJavaのコードよりもSQLでそのまま定義していくほうが好みです(FlywayではJavaコードで記述することもできます)

カスタムタスクの作成

カスタムタスクを作るためには、独立したプロジェクトとして作ったり、カスタムタスククラスを作ったりといった選択肢がありますが、幸い、FlywayのAPIはシンプルなものなので、一番コンパクトにビルドスクリプト(build.gradle)に直接記述することにしました。

FlywayのコアAPIのうち、clean, init, migrate, info, repairの5つを、それぞれflywayClean, flywayInit, flywayMigrate, flywayInfo, flywayRepairという名前でタスクとして定義します。

作成したビルドスクリプトが以下になります。

import com.googlecode.flyway.core.Flyway
import com.googlecode.flyway.core.api.MigrationInfo
import com.googlecode.flyway.core.api.MigrationInfoService
import com.googlecode.flyway.core.util.DateUtils

buildscript {
    repositories { mavenCentral() }
    dependencies {
        classpath files('src/main/resources')
        classpath 'com.googlecode.flyway:flyway-core:2.1'
        classpath 'postgresql:postgresql:9.1-901.jdbc4'
    }
}

def flyway = new Flyway()
flyway.setDataSource(jdbcUrl, jdbcUser, jdbcPassword)

task flywayInit << { flyway.init() }

task flywayClean << { flyway.clean() }

task flywayMigrate << { flyway.migrate() }

task flywayRepair << { flyway.repair() }

task flywayInfo << {
    def migrationInfoService = flyway.info()
    migrationInfoService.all().each{
        println "Version : " + it.getVersion()
        def description = it.getDescription() == null ? "" : it.getDescription()
        println "  Description  : " + description
        println "  Installed on : " + DateUtils.formatDateAsIsoString(it.getInstalledOn())
        println "  State        : " + it.getState().name()
    }
}

ハマった点としては、Flywayはデフォルトでクラスパス上からのdb.migrationの位置にあるSQLファイルを使ってマイグレーションを行うため、

    classpath files('src/main/resources')

としてクラスパスを指定しておかないと、タスクを実行してもエラーになってしまった点。それ以外は、まぁ、こんなふうに書けるんだぁという雰囲気は分かるかなぁと。。

ビルドスクリプト内の下記の行で、gradle.propertiesで定義したプロパティを利用しています。

    flyway.setDataSource(jdbcUrl, jdbcUser, jdbcPassword)

ビルドスクリプトを複数ファイルに分ける

上記は直接build.gradleに記述してももちろんよいのですが、可読性と保守性を少しばかり向上させるため、build.gradleと同じ階層のディレクトリにflyway.gradleという独立したファイルとして記述します。

こうすると、build.gradleでは、

apply from: "flyway.gradle"

のように一行書くだけで、新らしく定義したflywayInit, flywayClean, flywayInfo, flywayMigrate, flywayRepairといったタスクが利用可能になります。


実行してみる

5つほどタスクを作りましたが、日常的な利用としては、flywayMigrateタスクだけ実行しておくようにすればよいです。

実行してみると以下のとおり出力され、無事にビルドが成功しました。

[sts] -----------------------------------------------------
[sts] Starting Gradle build for the following tasks: 
[sts]      :flywayMigrate
[sts] -----------------------------------------------------
:flywayMigrate

BUILD SUCCESSFUL

Total time: 0.907 secs
[sts] -----------------------------------------------------
[sts] Build finished succesfully!
[sts] Time taken: 0 min, 0 sec
[sts] -----------------------------------------------------

データベースを見ると、無事にpersonテーブルが作られているようです。

Jenkinsへの適用

他のタスクと同様に、Jenkinsでも利用可能です。

以前作ったジョブに、flywayMigrate、flywayInfoの2つのGradleタスクを実行させるように設定を変更してジョブを実行すると、flywayInfo実行時の出力としてマイグレーション情報がコンソール出力されています。


まとめ

Gradleでマイグレーションを行えるようにしたい、という目標を達成するのに、思いの他色々勉強することができました。

  • gradle.propertiesに書いておくと、そのままビルドスクリプト上でもプロパティ名でアクセスできる。
  • ビルドスクリプトを分けた場合、build.gradleに「apply from: "ファイル名"」で組み込める。
  • buildscript {}ブロックを使って、カスタムクラスのクラスパスを定義する。
  • ビルドスクリプトもGroovyなので、クラスパスが通っておけばタスクとして自由にJava, Groovyコードを実行できる。
    • 当たり前っぽいことだけど、実際に作ってみると、改めて、相当強力なことができそうだなぁと感じました。

とはいえ、やはり理想は

apply plugin: "flyway"

だけで使えるようになることですね…。

おまけ。今回作ったコードもGitHub上に公開してあります。