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 を作成する
CREATE TABLE HOGE (
ID INT,
VALUE VARCHAR(12)
);
設定ファイルを作成する
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).
データベースの様子
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
として出力される。
例
CREATE TABLE HOGE (
ID INT,
VALUE VARCHAR(12)
);
INSERT INTO HOGE VALUES (1, 'hoge');
CREATE TABLE FUGA (
ID INT,
VALUE VARCHAR(12)
);
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 には migrate
や info
のようにコマンドが用意されている。
以下が、用意されているコマンドの一覧。
コマンド | 内容 |
---|---|
migrate | マイグレーションを実行する |
clean | データベース上の全てのオブジェクトを削除する。これには、 SQL ファイルで定義されていない、手動で作成したオブジェクトも含まれる。 |
info | マイグレーションがどのバージョンまで実行されているかの情報を表示する。 |
validate | データベースのバージョンと、用意されている SQL ファイルのバージョンに差異が無いかを確認する。差異がある場合はエラーが発生する。 |
init | Flyway 用のメタデータテーブルだけを作成し、データベースのバージョン1にする(バージョン番号はオプションで変更可能)。 |
repair | 状態が Failed になっているバージョンのメタデータを削除する(詳細後述)。 |
repair コマンド
あるバージョンの SQL ファイルがマイグレーションに失敗した場合、メタデータ上では、そのバージョンの Status が Failed
になる。
> 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 プログラムに組み込んで利用することができる。
プログラム
repositories {
mavenCentral()
}
dependencies {
compile 'org.hsqldb:hsqldb:2.3.2'
compile 'org.flywaydb:flyway-core:3.0'
}
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 のプラグインとして利用する
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 プラグインと合わせて使用してみる。
実装方法
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"
}
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 ドライバが必要になる。
dependencies {
compile 'org.flywaydb:flyway-core:3.0'
compile 'org.sqldroid:sqldroid:1.0.3'
}
マイグレーション用の SQL ファイルを配置する
マイグレーション用の SQL ファイルは、 assets/db/migration
の下に配置する。
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');
マイグレーション処理を実装する
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();
}
}
実行結果
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 を保存している)ので、実装を確認してみると、以下のようになっていた。
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
を使った実装に切り替えてみる。
実装
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 + "]";
}
}
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 */}
}
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 でマイグレーションを行っている。