LoginSignup
271

Flyway使い方メモ

Last updated at Posted at 2014-08-02

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

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
271