Edited at

Flyway使い方メモ

More than 3 years have 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 でマイグレーションを行っている。


参考