FlywayをJavaプログラムから使ってみる、その2

FlywayをJavaプログラムから使ってみる、その1の続きである。

Javaプログラムによるマイグレーション

SQLによるマイグレーションは確認したが、Javaプログラムからも行ってみる。これの何がうれしいかというと、SQLでは記述できないことが記述できることだ。

例えば、いったんはBLOBにJSONとして様々な値を格納したが、遅いのでやっぱりそのJSONの中身をテーブルフィールドにしたい等という要求がある。あるいはその逆もだ。

これは簡単だった。先のFlywayの命名規則に沿っていれば、SQLファイルだろうがJavaクラスだろうが区別なく扱ってくれる。先の例のドットを下線に変更し、二番目をJavaで記述する。

として、後者は以下とする。

package db.migration;

import java.sql.*;

import org.flywaydb.core.api.migration.*;

public class V2019_07_13_02__changing_table extends BaseJavaMigration {
  public void migrate(Context context) throws Exception {
    try (Statement stmt = context.getConnection().createStatement()) {
      stmt.execute("alter table car add remarks varchar(80)");
      stmt.execute("INSERT INTO car (id, name, color) VALUES (2, 'Isuzu', 'blue')");
    }
  }
}

DBを削除してやり直してみると、ちゃんとこの通りに実行されている。

マイグレーションに失敗したらどうなるのか?

次のような失敗するSQLを書いてみる。

V2019_07_13_03__failing.sql

alter table car drop remarks;
alter table car drop nothing;

当然エラーになる。

Exception in thread "main" org.flywaydb.core.internal.command.DbMigrate$FlywayMigrateException: 
Migration V2019_07_13_03__failing.sql failed
--------------------------------------------
SQL State  : 42000
Error Code : 1091
Message    : (conn=12) Can't DROP COLUMN `nothing`; check that it exists
Location   : db/migration/V2019_07_13_03__failing.sql (C:\devel\workspace\flyway\bin\default\db\migration\V2019_07_13_03__failing.sql)
Line       : 2
Statement  : alter table car drop nothing

    at org.flywaydb.core.internal.command.DbMigrate.doMigrateGroup(DbMigrate.java:370)
    at org.flywaydb.core.internal.command.DbMigrate.access$200(DbMigrate.java:54)

テーブルの方を見てみると、remarks列は削除されている。もう一度実行してみると、今度は以下のエラー。

Exception in thread "main" org.flywaydb.core.api.FlywayException: Validate failed: Detected failed migration to version 2019.07.13.03 (failing)
    at org.flywaydb.core.Flyway.doValidate(Flyway.java:1482)
    at org.flywaydb.core.Flyway.access$100(Flyway.java:85)
    at org.flywaydb.core.Flyway$1.execute(Flyway.java:1364)
    at org.flywaydb.core.Flyway$1.execute(Flyway.java:1356)
    at org.flywaydb.core.Flyway.execute(Flyway.java:1711)
    at org.flywaydb.core.Flyway.migrate(Flyway.java:1356)
    at flywaytest.Main.main(Main.java:11)

どうやって復旧するのだろうか?

とりあえず、適用履歴を見てみると、たしかに3つ目は失敗している。

MariaDB [mydb]> select version, checksum, type, success from flyway_schema_history;
---------------+------------+------+---------+
 version       | checksum   | type | success |
---------------+------------+------+---------+
 2019.07.13.01 | -223036259 | SQL  |       1 |
 2019.07.13.02 |       NULL | JDBC |       1 |
 2019.07.13.03 | -710948869 | SQL  |       0 |
---------------+------------+------+---------+
 rows in set (0.001 sec)

あちこちに記述があるのだが、FlywayはSQLファイルのチェックサムをとっているので、単純にSQLファイルを修正して再実行してはいけないという。しかし、このテーブルを見てみれば、最後のエントリを削除し、削除されたフィールドを復旧し、SQLを修正すればいけそうな気がする。

delete from flyway_schema_history where version='2019.07.13.03';
alter table car add remarks varchar(80);

と修正し、先のSQLを以下に修正してみる。

alter table car drop remarks;
alter table car drop color;

これでうまくいった。

repairを使う

上記のようなことを行わなくとも、repair()を呼び出せば失敗した履歴を削除してくれるらしい。そしてrepair()は問題がなくとも呼び出して構わないらしい。

そこでいったんDBを削除し、再作成し、Javaコードを以下にして、再度実行してみる。

public class Main {

  public static void main(String[]args) throws Exception {
    Class.forName("org.mariadb.jdbc.Driver");
    String url = "jdbc:mariadb://localhost/mydb?useUnicode=true&characterEncoding=utf8";
    Flyway flyway = Flyway.configure().dataSource(url, "root", "root").load();
    flyway.repair();
    flyway.migrate();
  }
}

当然、03でエラーが表示される。既にremarks列は削除されているので、03を以下に修正してみる。

alter table car drop color;

もう一度実行すると。意図通りの結果になる。

※もちろん、これはやってはいけない。DBの状態を02時点までいったん戻してから、03を元の意図通りに修正する必要がある。

複数開発者によるマイグレーション

FlywayはDDL文を順番に実行していくことを前提としているが、複数開発者のいる場合はそうもいかないこともある。この場合はどうなるのか?以下に例がある。

最後の教訓にあるが「conflictして修正した方が安全」だろう。つまり、これまで例として使ってきた日付方式ではなく、連番方式の方が安全だ。つまり、

V1__foo.sql
V2__bar.sql
V3__foobar.sql

などと必ず連番で構成するということだ。ちなみに、AさんがV3_foobar.sqlを作成し、BさんがV3_sample.sqlを作成した場合、Flywayはそれらを同じものとみなすので実行時にエラーを出してくれる。必ずしも、git上あるいはsvn上でconflictする必要は無いということだ。