More than 1 year has passed since last update.

Java Magazine の第 17 号でも紹介されている噂?の Flyway を使ってみる。

Flyway とは

Flyway は、オープンソースのデータベースマイグレーションツール。

Flyway を使うことで、データベースの状態をバージョン管理できるようになる。

Flyway (マイグレーションツール)を使う理由

データベースを使った開発をしていると、以下のような問題が往々にして発生する。

よくある問題

  • あるデータベースの、現在の状態が分からない。
  • あるパッチ用 SQL が、データベースに既に適用されているか分からない。
  • 本番環境で緊急対応が必要になり適用したパッチが、テスト環境にも反映されているか分からない。
  • 新しいデータベース環境を作成するときに、どの SQL を、どの順番で実行すればいいか分からない。

そんなときに、 Flyway のようなデータベースをバージョン管理するツールがあると便利。

パッチを適用したかどうかは、 Flyway が管理しているし、簡単に確認できる。
新しいデータベース環境も、 Flyway を実行すれば簡単に作成できる。

Hello World

インストール

Flyway

こちら から、flyway-commandline-3.0.zipをダウンロードする。

ダウンロードが完了したら、 zip を解凍する。

解凍後の状態
|-bin/
|-conf/
|-jars/
|-sql/
|-flyway
|-flyway.cmd
|-LICENSE.txt
`-README.txt

以降、この解凍先フォルダを %FLYWAY_DIR% と表記する。

データベース

HSQLDB を使用する。

こちら から、 hsqldb-2.3.2.zip をダウンロードする。

ダウンロードが完了したら、 zip を解凍し、 lib の下の hsqldb.jar を取得する。

Java

割愛。

マイグレーション

SQL を作成する

V1__create_table.sql
CREATE TABLE HOGE (
    ID INT,
    VALUE VARCHAR(12)
);

設定ファイルを作成する

flyway.properties
flyway.user=SA
flyway.url=jdbc:hsqldb:file:./db/sample

ファイルを配置する

以下のようにファイルを配置する。

ファイル 配置先
V1__create_table.sql %FLYWAY_DIR%\sql
flyway.properties %FLYWAY_DIR%\conf
hsqldb.jar %FLYWAY_DIR%\jars
フォルダ構成
%FLYWAY_DIR%/
  |-sql/
  |  `-V1__create_table.sql
  |-conf/
  |  `-flyway.properties
  |-jars/
  |  `-hsqldb.jar
  :
  :

マイグレーションを実行する

コマンドラインから、 %FLYWAY_DIR% に移動し、以下のコマンドを実行する。

> flyway migrate
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)
Validated 1 migration (execution time 00:00.010s)
Creating Metadata table: "PUBLIC"."schema_version"
Current version of schema "PUBLIC": << Empty Schema >>
Migrating schema "PUBLIC" to version 1
Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.020s).

データベースの様子

flyway.JPG

HOGE テーブルが作成されている。

現在のバージョンを確認する

以下のコマンドを実行する。

> flyway info
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)

+----------------+--------------+---------------------+---------+
| Version        | Description  | Installed on        | State   |
+----------------+--------------+---------------------+---------+
| 1              | create table | 2014-08-02 11:40:21 | Success |
+----------------+--------------+---------------------+---------+

説明

Flyway では、データベースに適用する各 SQL ファイルをバージョンごとに作成して、管理する。
ファイル名は書式が決められており、バージョン番号から始まる形で指定する。

バージョン番号は一意である必要があり、同じバージョンの SQL ファイルを用意することはできない。

flyway migrate コマンドで、マイグレーションが実行され、まだデータベースに適用されていないバージョンの SQL が適用される。
既に適用済みの SQL は、 Flyway が判断して適用しないようにしてくれる。

これによって、データベースのバージョン管理・マイグレーションがスムーズにできるようになる。

バージョン番号について

書式

V<Version>__<Description>.sql

V

SQL ファイルの先頭は、必ず V から始める。

<Version>

バージョン番号。

半角数値と、ドット . またはアンダーバー _ の組み合わせで指定する。

アンダーバーは、実行時にドットに変換される。

2
2.1.0
3_1_2

__

「バージョン番号」と「説明」とを区切る部分。

アンダーバーを2つ続ける。

<Description>

説明。

そのバージョンの説明を記述する。

アンダーバーは実行時に空白スペースに置き換わる。

説明は、 flyway info コマンドを実行した時に Description として出力される。

V1.0__create_table.sql
CREATE TABLE HOGE (
    ID INT,
    VALUE VARCHAR(12)
);
V1.1__insert_data.sql
INSERT INTO HOGE VALUES (1, 'hoge');
V2_0__create_table.sql
CREATE TABLE FUGA (
    ID INT,
    VALUE VARCHAR(12)
);
V2_1__insert_data.sql
INSERT INTO FUGA VALUES (1, 'fuga');
マイグレーションの実行
> flyway migrate
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)
Validated 4 migrations (execution time 00:00.010s)
Creating Metadata table: "PUBLIC"."schema_version"
Current version of schema "PUBLIC": << Empty Schema >>
Migrating schema "PUBLIC" to version 1.0
Migrating schema "PUBLIC" to version 1.1
Migrating schema "PUBLIC" to version 2.0
Migrating schema "PUBLIC" to version 2.1
Successfully applied 4 migrations to schema "PUBLIC" (execution time 00:00.090s).

> flyway info
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)

+----------------+--------------+---------------------+---------+
| Version        | Description  | Installed on        | State   |
+----------------+--------------+---------------------+---------+
| 1.0            | create table | 2014-08-02 12:16:28 | Success |
| 1.1            | insert data  | 2014-08-02 12:16:28 | Success |
| 2.0            | create table | 2014-08-02 12:16:28 | Success |
| 2.1            | insert data  | 2014-08-02 12:16:28 | Success |
+----------------+--------------+---------------------+---------+

コマンド

Flyway には migrateinfo のようにコマンドが用意されている。

以下が、用意されているコマンドの一覧。

コマンド 内容
migrate マイグレーションを実行する
clean データベース上の全てのオブジェクトを削除する。これには、 SQL ファイルで定義されていない、手動で作成したオブジェクトも含まれる。
info マイグレーションがどのバージョンまで実行されているかの情報を表示する。
validate データベースのバージョンと、用意されている SQL ファイルのバージョンに差異が無いかを確認する。差異がある場合はエラーが発生する。
init Flyway 用のメタデータテーブルだけを作成し、データベースのバージョン1にする(バージョン番号はオプションで変更可能)。
repair 状態が Failed になっているバージョンのメタデータを削除する(詳細後述)。

repair コマンド

あるバージョンの SQL ファイルがマイグレーションに失敗した場合、メタデータ上では、そのバージョンの Status が Failed になる。

マイグレーションに失敗した状態のStatus
> flyway info
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)

+----------------+---------------+---------------------+---------+
| Version        | Description   | Installed on        | State   |
+----------------+---------------+---------------------+---------+
| 1.0            | create table  | 2014-08-02 12:32:26 | Success |
| 1.1            | insert data   | 2014-08-02 12:33:08 | Failed  |
| 2.0            | create table  |                     | Pending |
| 2.1            | insert data   |                     | Pending |
+----------------+---------------+---------------------+---------+

この状態では、たとえ問題の SQL ファイルを修正しても、 migrate を実行するとエラーになる。

エラーを取り除くためには、 repair コマンドを実行する。

> flyway repair
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)
Metadata table "PUBLIC"."schema_version" successfully repaired (execution time 00:00.000s).
Manual cleanup of the remaining effects the failed migration may still be required.

> flyway info
Flyway (Command-line Tool) v.3.0

Database: jdbc:hsqldb:file:./db/sample (HSQL Database Engine 2.3)

+----------------+--------------+---------------------+---------+
| Version        | Description  | Installed on        | State   |
+----------------+--------------+---------------------+---------+
| 1.0            | create table | 2014-08-02 12:32:26 | Success |
| 1.1            | insert data  |                     | Pending |
| 2.0            | create table |                     | Pending |
| 2.1            | insert data  |                     | Pending |
+----------------+--------------+---------------------+---------+

様々な実行方法

コマンドラインから利用する

「Hello World」で説明した方法が、コマンドラインから実行する方法になる。

Java プログラムに組み込んで利用する

Flyway は Java プログラムに組み込んで利用することができる。

プログラム

build.gradle
repositories {
    mavenCentral()
}

dependencies {
    compile 'org.hsqldb:hsqldb:2.3.2'
    compile 'org.flywaydb:flyway-core:3.0'
}
Main.java
package sample.flyway;

import org.flywaydb.core.Flyway;

public class Main {

    public static void main(String[] args) throws Exception {
        Flyway flyway = new Flyway();

        flyway.setDataSource("jdbc:hsqldb:file:./db/sample", "SA", "");

        flyway.migrate();
    }
}
フォルダ構成
`-src/
  `-main/
    |-java/
    |  `-sample/
    |    `-flyway/
    |      `-Main.java
    `-resources/
      `-db/
        `-migration/
          `-V1__create_table.sql

SQL ファイルは、 クラスパス以下の db/migration の下に配置する。

これを実行すると、マイグレーションが行われる。

Gradle のプラグインとして利用する

build.gradle
apply plugin: 'java'
apply plugin: 'flyway'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.hsqldb:hsqldb:2.3.2"
        classpath "org.flywaydb:flyway-gradle-plugin:3.0"
    }
}

flyway {
    user = 'SA'
    url = 'jdbc:hsqldb:file:./database/sample'
}
フォルダ構成
|-build.gradle
`-src/
  `-main/
    `-resources/
      `-db/
        `-migration/
          `-V1__create_table.sql
実行
> gradle flywayMigrate
:flywayMigrate
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayMigrate

BUILD SUCCESSFUL

Total time: 4.084 secs

> gradle flywayInfo
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayInfo
+----------------+----------------------------+---------------------+---------+
| Version        | Description                | Installed on        | State   |
+----------------+----------------------------+---------------------+---------+
| 1              | create table               | 2014-08-02 14:50:01 | Success |
+----------------+----------------------------+---------------------+---------+

BUILD SUCCESSFUL

Total time: 3.792 secs

buildscript で Flyway の Gradle プラグインを追加し、 appliy plugin: 'flyway' とすることで、 Gradle から Flyway を使えるようになる。

SQL ファイルは実行時にクラスパスが通っている場所から見て db/migration の下に配置する。
つまり、 src/main/resources/db/migration の下に配置すれば、 Flyway が検出できる(場所はオプションで変更可能)。

Java でマイグレーションを実装する

LOB データを扱う必要があったり、マイグレーションの方法が複雑な場合は、 Java でマイグレーション処理を実装することができる。

ここでは、 Gradle プラグインと合わせて使用してみる。

実装方法

build.gradle
apply plugin: 'java'
apply plugin: 'flyway'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.hsqldb:hsqldb:2.3.2"
        classpath "org.flywaydb:flyway-gradle-plugin:3.0"
    }
}

flyway {
    user = 'SA'
    url = 'jdbc:hsqldb:file:./database/sample'
}

repositories {
    mavenCentral()
}

dependencies {
    compile "org.flywaydb:flyway-gradle-plugin:3.0"
}
V2__insert_data.java
package db.migration;

import java.sql.Connection;
import java.sql.Statement;

import org.flywaydb.core.api.migration.jdbc.JdbcMigration;

public class V2__insert_data implements JdbcMigration {

    @Override
    public void migrate(Connection connection) throws Exception {

        try (Statement statement = connection.createStatement();) {
            statement.executeUpdate("INSERT INTO HOGE VALUES (1, 'hoge')");
        }

    }
}
フォルダ構成
|-build.gradle
`-src/
  `-main/
    |-java/
    | `-db/
    |   `-migration/
    |     `-V2__insert_data.java
    `-resources/
      `-db/
        `-migration/
          `-V1__create_table.sql

実行

実行
> gradle flywayMigrate
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayMigrate

BUILD SUCCESSFUL

Total time: 4.002 secs

> gradle flywayInfo
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:flywayInfo
+----------------+----------------------------+---------------------+---------+
| Version        | Description                | Installed on        | State   |
+----------------+----------------------------+---------------------+---------+
| 1              | create table               | 2014-08-02 14:53:43 | Success |
| 2              | insert data                | 2014-08-02 14:53:43 | Success |
+----------------+----------------------------+---------------------+---------+

BUILD SUCCESSFUL

Total time: 4.01 secs

説明

Java でマイグレーションを実装する場合は、 SQL ファイルと同じように定められた書式でクラス名を定義する(V2__insert_data.java)。

マイグレーション用のクラスは、 JdbcMigration インターフェースを実装し、 migrate() メソッドでマイグレーションの処理を実装する。

Android で使用する

Flyway は Android に組み込んで利用することもできる。

必要なファイル

flyway 本体(flyway-core)の他に SQLDroid という Android 用の JDBC ドライバが必要になる。

build.gradle
dependencies {
    compile 'org.flywaydb:flyway-core:3.0'
    compile 'org.sqldroid:sqldroid:1.0.3'
}

マイグレーション用の SQL ファイルを配置する

マイグレーション用の SQL ファイルは、 assets/db/migration の下に配置する。

flyway.JPG

V1__create_database.sql
CREATE TABLE SAMPLE_TABLE (
    ID INTEGER PRIMARY KEY AUTOINCREMENT,
    CODE,
    NAME
);

INSERT INTO SAMPLE_TABLE (CODE, NAME) VALUES ('hoge', 'HOGE');
INSERT INTO SAMPLE_TABLE (CODE, NAME) VALUES ('fuga', 'FUGA');
INSERT INTO SAMPLE_TABLE (CODE, NAME) VALUES ('piyo', 'PIYO');

マイグレーション処理を実装する

MainActivity.java
package com.example.flywaysample;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.android.ContextHolder;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // マイグレーション処理
        SQLiteDatabase db = this.openOrCreateDatabase("sample.db", MODE_PRIVATE, null);
        ContextHolder.setContext(this.getApplicationContext());

        Flyway flyway = new Flyway();
        flyway.setDataSource("jdbc:sqlite:" + db.getPath(), "", "");
        flyway.migrate();
        Log.v("FlywaySample", "migrate database.");

        // データ取得
        Cursor cursor = db.rawQuery("SELECT * FROM SAMPLE_TABLE", null);
        boolean next = cursor.moveToFirst();

        while (next) {
            int id = cursor.getInt(0);
            String code = cursor.getString(1);
            String name = cursor.getString(2);

            Log.v("FlywaySample", String.format("id=%d, code=%s, name=%s%n", id, code, name));

            next = cursor.moveToNext();
        }

        cursor.close();
        db.close();
    }
}

実行結果

LogCat
10-12 07:50:40.385: V/FlywaySample(11986): migrate database.
10-12 07:50:40.385: V/FlywaySample(11986): id=1, code=hoge, name=HOGE
10-12 07:50:40.385: V/FlywaySample(11986): id=2, code=fuga, name=FUGA
10-12 07:50:40.395: V/FlywaySample(11986): id=3, code=piyo, name=PIYO

ContextHolder.setContext() について

1点要注意なのが、

ContextHolder.setContext(this.getApplicationContext());

この部分。

ContextHolder は、名前の通り Context を保持しておくためのクラスで、 Flyway が DB マイグレーション時に assets/db/migration から SQL ファイルを取得するのに使用している。

なんだか嫌な臭いがプンプンした(static メソッドで Context を保存している)ので、実装を確認してみると、以下のようになっていた。

ContextHolder.java(コメント除去)
package org.flywaydb.core.api.android;

import android.content.Context;

public class ContextHolder {
    private ContextHolder() {}

    private static Context context;

    public static Context getContext() {
        return context;
    }

    public static void setContext(Context context) {
        ContextHolder.context = context;
    }
}

static フィールドに Context を保存している。
つまり、もし setContext() に Activity のインスタンスを渡してしまうと Activity 終了後も参照が残ってしまい、メモリリークが発生する危険性がある

マイグレーションが終わったら必ず setContext(null) するという手もあるが、最初から ApplicationContext を渡すようにするのがいいと思う。

Flyway の公式ドキュメントに書かれている通りに実装すると、 Acitivty のインスタンスを渡してしまうので危険です。

SQLiteOpenHelper を使った実装

Activity に直接データベース処理を書くのはお行儀が悪いので、 SQLiteOpenHelper を使った実装に切り替えてみる。

実装

SampleTable
package com.example.flywaysample;

public class SampleTable {

    private int id;
    private String code;
    private String name;

    public SampleTable(int id, String code, String name) {
        this.id = id;
        this.code = code;
        this.name = name;
    }

    @Override
    public String toString() {
        return "SampleTable [id=" + id + ", code=" + code + ", name=" + name + "]";
    }
}
MyDatabaseHelper.java
package com.example.flywaysample;

import java.util.ArrayList;
import java.util.List;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.android.ContextHolder;

import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class MyDatabaseHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "sample.db";
    private static final int DATABASE_VERSION = 1;

    public MyDatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);

        SQLiteDatabase db = getWritableDatabase();
        ContextHolder.setContext(context.getApplicationContext());

        Flyway flyway = new Flyway();
        flyway.setDataSource("jdbc:sqlite:" + db.getPath(), "", "");
        flyway.migrate();

        Log.v("FlywaySample", "migrate database.");

        db.close();
    }

    public List<SampleTable> findAll() {
        SQLiteDatabase db = getReadableDatabase();

        Cursor cursor = db.rawQuery("SELECT * FROM SAMPLE_TABLE", null);
        boolean next = cursor.moveToFirst();

        List<SampleTable> list = new ArrayList<SampleTable>();

        while (next) {
            int id = cursor.getInt(0);
            String code = cursor.getString(1);
            String name = cursor.getString(2);

            list.add(new SampleTable(id, code, name));

            next = cursor.moveToNext();
        }

        cursor.close();
        db.close();

        return list;
    }

    @Override public void onCreate(SQLiteDatabase db) {/* no use */}
    @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {/* no use */}
}
MainActivity.java
package com.example.flywaysample;

import java.util.List;

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyDatabaseHelper dbHelper = new MyDatabaseHelper(this);
        List<SampleTable> list = dbHelper.findAll();

        for (SampleTable sample : list) {
            Log.v("FlywaySample", sample.toString());
        }
    }
}

説明

  • マイグレーション処理は Flyway が管理するので、 onCreate()onUpgrade() は使用しない。
  • 代わりに、コンストラクタの時点で getWritableDatabase() して、 Flyway でマイグレーションを行っている。

参考