kagamihogeの日記

kagamihogeの日記です。

JEP 325: Switch Expressionsをテキトーに訳した

http://openjdk.java.net/jeps/325 をテキトーに訳した。

JEP 325: Switch Expressions

Author   Brian Goetz
Owner   Jan Lahoda
Created 2017/12/04 08:56
Updated 2018/01/29 16:23
Type    Feature
Status  Candidate
Component   specification/language
Scope   SE
Discussion  amber dash dev at openjdk dot java dot net
Effort  M
Duration    M
Priority    3
Reviewed by Alex Buckley
Issue   8192963

Summary

switchを文でも式としても使えるよう拡張し、また、nullの扱い方を改良します。この変更は今までの書き方を単純化するもので、switchパターンマッチング(JEP 305)に備える目的もあります。

Motivation

我々はJava言語にパターンマッチング(JEP 305)を導入するための拡張の準備をしていますが、既存のswitchの微妙ないくつかの点が障害となっており、これはユーザを長年イライラさせ続けているものでもあります。これにはnullの処理(switchは引数がnullだとNullPointerExceptionをスロー)と、swtichが文でしか使えない、があります。場合にもよりますが、式として複数条件分岐*1を表現する方が自然な場合があります。

既存のswitch文の多くの使われ方は本質的にはswitch式のシミュレーションで、各arm*2は共用のターゲット変数に代入するか値を返すかのどちらかをします。

int numLetters;
switch (day) {
    case MONDAY:
    case FRIDAY:
    case SUNDAY:
        numLetters = 6;
        break;
    case TUESDAY:
        numLetters = 7;
        break;
    case THURSDAY:
    case SATURDAY:
        numLetters = 8;
        break;
    case WEDNESDAY:
        numLetters = 9;
        break;
    default:
        throw new IllegalStateException("Wat: " + day);
};

文としてこれを表現するのは、似たようなことの繰り返しになるので、回りくどく間違いやすいです。上記コードの意図は日にちに対応するnumLettersの値を計算をする、というものです。これは、より直感的で、明瞭・安全に書けるのが望ましいです。

int numLetters = switch (day) {
    case MONDAY, FRIDAY, SUNDAY -> 6;
    case TUESDAY -> 7;
    case THURSDAY, SATURDAY -> 8;
    case WEDNESDAY -> 9;
};

switch式への拡張は更に別の要求へと繋がっています。単純なORパターン形式(上の例で見たもの)、フローアナリシスの拡張(式は常に値を計算するか即時終了する*3)、switch式の各caseで値の算出ではなく例外をスロー可能にする、などです。

Description

式として使えるようにswitch文を拡張します。よくある例としては、switch式は以下のようになります。

T result = switch (arg) {
    case L1 -> e1;
    case L2 -> e2;
    default -> e3;
}

switch式は複合的な式(poly expression)になります。ターゲット型が既知であれば、その型は各armにプッシュダウンします。既知であればswitch式の型はそのターゲット型となり、そうでない場合、各caseの型との組み合わせでスタンドアローンの型が算出されます*4

switch文では、breakがswitchの実行を停止させます。switchで使うために、breakが引数を取れるよう拡張し、その値はswitch式の値になります。

switch式のcaseはbreakを介して式で値を算出しつつ文の実行も出来ます。その場合switch文のように複数の文を書きます。

int result = switch (s) {
    case "Foo":
        break 1;
    case "Bar":
        break 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 3;
}

ただし、上記のような例は稀で、文は無く単一の式を評価する複数のcase、を想定しています。また、シンタックスシュガーを提供するので、その場合はswitch式は以下のように定義します。

case LABEL -> expression;

元はこうです。

case LABEL: break expression;

よって、上記例は簡潔に書けます。

int result = switch (s) {
    case "Foo" -> 1;
    case "Bar" -> 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 3;
}

また、switch式では以下のようにも書けます。

case LABEL -> throw e;

元はこうです。

case LABEL: throw e;

よって、以下のようになります。

int result = switch (s) {
    case "Foo" -> 1;
    case "Bar" -> 2;
    default -> throw new IllegalStateException(s);
}

これらの書き方は必要に応じて使い分けられるので、上記のswitch式は以下のようにも書けます。

int result = switch (s) {
    case "Foo" -> 1;
    case "Bar" -> 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        break 3;
}

breakの2つの形式(値有・無)はメソッドのreturnの2つの形式と似ています。両者ともメソッド実行を即時停止し、非voidメソッドでは加えて、メソッドの呼び出し元に値を返す必要があります。(break expression-valuebreak labelのあいまいさは比較的簡単に処理可能です)

switch式のcaseは網羅的である必要があり、とりうる入力に対しarmのうち一つだけが評価される必要があります。これは基本的にはdefault句が必要という意味ですが、enumのswitch式で取りうる値をすべて網羅する場合(and eventually, switch expressions over sealed types)、default句はコンパイラが挿入して、コンパイラenum定義がコンパイル時と実行時で変わったことを示します。(これは現状開発者が手でやっていますが、コンパイラによる挿入は面倒を省いてくれて、エラーメッセージは手で書いたよりも記述的となります)

更にswitchの改良を推し進めて、switchの式と文をある程度同じにします。case句で一つしか書けないのをカンマ区切りのリストで書けるようにします。

switch (day) {
    case MONDAY, FRIDAY, SUNDAY: 
        numLetters = 6;
        break;
    ...
};

引数が参照型(現状ではプリミティブのラッパー・文字列・enumのみ)のswitchでは、case句は明示的にnullを指定できます。

String formatted = switch (s) {
    case null -> "(null)";
    case "" -> "(empty)";
    default -> s;
}

もしswitchcase null句が無い場合、最初のcase句は以下であると解釈されます。

case null: throw new NullPointerException();

この挙動はswitchで参照型を用いる際の現行の振る舞いと一致します。

なおもし余裕があれば、switchで現行では使えないプリミティブ型、float, double, longなど、を使えるようにする拡張もするかもしれません。

Dependencies

Pattern Matching (JEP 305)はこのJEPに依存します。

*1:multi-way conditionalsが原文。訳は俺が勝手につけたもの。言うまでもなくswitch-caseのような複数の条件にマッチするヤツのこと

*2:switchからcaseが伸びる様をarmと表現してると思われ

*3:compute a value or complete abruptlyが原文。良い日本語思い浮かばず

*4:この辺良くわからん。

FlywayのdocumentationのConceptsを読んだ

https://flywaydb.org/documentation/ を読んだ。

Overview

Flywayはデータベースマイグレーションをやりやすくします。

Tip: すぐに読み終えられるのでGet Startedセクションを先に目を通すことをオススメします。

Flywayはオープンソースのデータベースマイグレーションツールです。単純さとCoCに特に主眼を置いています。

Flywayは7つの基本コマンド群から構成されています。Migrate, Clean, Info, Validate, Undo, Baseline and Repair.

マイグレーションSQL(データベース固有の文法(PL/SQL, T-SQL)もサポート)もしくはJava(複雑なデータ変換やLOBの処理など向け)で書きます。

Flywayにはコマンドラインクライアント](https://flywaydb.org/documentation/commandline)があります。JVM上で動かしてアプリケーション開始時にDBをマイグレーションするにはJava APIAndroid上でも動作)の使用を推奨します。もしくは、Maven pluginGradle pluginを使います。

上記で不足する場合、Spring Boot, Dropwizard, Grails, Play, SBT, Ant, Griffon, Grunt, Ninjaなどで使えるプラグインがあります。

サポート対象のDBは以下の通りです。Oracle, SQL Server(Amazon RDSとAzure SQL Database含む), DB2, MySQLAmazon RDS, Azure Database & Google Cloud SQL), MariaDB, PostgreSQLAmazon RDS, Azure Database, Google Cloud SQL & Heroku含む), Redshift, CockroachDB, SPAP HANA, Sybase ASE, H2, HSQLDB, Derby, SQLLite

Migrations

Overview

FlywaではDBに対するすべての変更をマイグレーション(migrations)と呼びます。マイグレーションはバージョン付き(versiond)かリピータブル(repeatable)の二種類あります。バージョン付きマイグレーションには二つの形式、通常(regular)と取消(undo)、があります。

バージョン付きマイグレーション(Versioned migrations)は、バージョン(version)と説明(description )とチェックサムchecksum)、で構成されます。バージョンは一意にする必要があります。説明は、個々のマイグレーションが何をしているかという、人間向けの単なる情報です。チェックサムは意図しない変更を検出するためのものです。バージョン付きマイグレーションマイグレーションの基本形です。マイグレーションは一度だけ順番に適用されます。

また、同一バージョンに取消マイグレーション(undo migration)を付けることでそのマイグレーションを取り消し可能にも出来ます。

リピータブルマイグレーション(Repeatable migrations)は説明とチェックサムを持ちますが、バージョンがありません。一度だけ実行するのではなく、代わりに毎回チェックサムの変更が(再)適用されます。

単一のマイグレーション実行では、リピータブルマイグレーションは、すべてのペンディング状態のバージョン付きマイグレーションが実行された後、常に最後に適用されます。リピータブルマイグレーションは説明の順に適用されます*1

デフォルトではバージョン付き・リピータブルマイグレーションのどちらもSQLJavaのどちらかで作成可能で、複数のステートメントを書けます。

FlywayはファイルシステムJavaのクラスパス上のマイグレーションを自動検出します。

どのマイグレーションがいつ誰によって適用されたかをトラッキングするために、Flywayはスキーマschema history tableを追加します。

Versioned Migrations

最もよく使うマイグレーションバージョン付きマイグレーションです。個々のバージョン付きマイグレーションは、バージョン・説明・チェックサム、を持ちます。バージョンは一意にする必要があります。説明は、個々のマイグレーションが何をしているかという、人間向けの単なる情報です。チェックサムは意図しない変更を検出するためのものです。バージョン付きマイグレーションマイグレーションの基本形です。マイグレーションは一度だけ順番に適用されます。

バージョン付きマイグレーションは基本的には以下の用途に使われます。

  • create/alter/drop table,index,foreign keys,enum,UDT
  • 参照データの更新(Reference data updates)
  • データ修正

以下は一例です。

CREATE TABLE car (
    id INT NOT NULL PRIMARY KEY,
    license_plate VARCHAR NOT NULL,
    color VARCHAR NOT NULL
);

ALTER TABLE owner ADD driver_license_id VARCHAR;

INSERT INTO brand (name) VALUES ('DeLorean');

バージョン付きマイグレーションにはそれぞれに一意のバージョンを割り当てる必要があります。一般的なドット記法に沿えば任意のバージョンが有効です。通常なら単純増加の整数で十分でしょう。ただし、Flywayは柔軟性があり以下のようなバージョン付きマイグレーションの番号が有効です。

  • 1
  • 001
  • 5.2
  • 1.2.3.4.5.6.7.8.9
  • 205.68
  • 20130115113556
  • 2013.1.15.11.35.56
  • 2013.01.15.11.35.56

バージョン付きマイグレーションはその番号順に適用されます。バージョンは数値としてソートされます。

Undo Migrations

Flyway Pro

取消マイグレーションは通常のバージョン付きマイグレーションの逆です。取消マイグレーションの役割は同一バージョンのマイグレーションの影響を取消すことにあります。取消マイグレーションは無くても良く、通常のバージョン付きマイグレーションの実行時には不要です。

上述の例を取ると、取消マイグレーションは以下のようになります。

DELETE FROM brand WHERE name='DeLorean';

ALTER TABLE owner DROP driver_license_id;

DROP TABLE car;

Important Notes

取消マイグレーションの考え方は魅力的に見えますが、実際には何らかを壊す場合があります。破壊的変更(drop, delete, truncate)はトラブルの元です。そうでないとしても、バックアップから復元するためのお手製のソリューションを十分にテストする羽目になります。

前提として、取消マイグレーションマイグレーション全体が成功している場合に取消を行うものです。そのためDDLトランザクション無しで失敗したバージョン付きマイグレーションをどうにかすることは出来ません。その理由は、マイグレーションがどの段階で失敗するかは分からないためです。いま10ステートメントあるとして、1,5,7,10番目が失敗するかもしれません。事前にこれを検知する方法はありません。これとは対照的に、取消マイグレーションはバージョン付きマイグレーション全体を取消します。よってそうした状況下をなんとかするものではありません。

我々が推奨する代替案はDBと現行の本番環境にデプロイされているコードの全バージョンとの間に後方互換性を持ち続けるというものです。この場合であればマイグレーションの失敗はトラブルになりません。アプリケーションの旧バージョンはDBと互換性が保たれているので、アプリケーションをロールバックし、調査して、しかるべき処置を行います。

この案は適切で、十分にテストした、バックアップとリストアによって実現します。特定のDBの構造に非依存で、テストして実証が取れたら、マイグレーションスクリプトは何も壊さなくなります。パフォーマンスの最適化には、使用しているインフラストラクチャがサポートしていれば、基底ストレージのスナップショット機能の使用を推奨します。特に大規模データボリュームでは、伝統的なバックアップとリストアよりも格段に高速となる場合があります。

Repeatable Migrations

リピータブルマイグレーションは説明とチェックサムを持ちますが、バージョンがありません。一度だけ実行する代わりに毎回チェックサムの変更が(再)適用されます。

定義を持つデータベースオブジェクトの管理に有用で、バージョン管理下の単一ファイルで扱えるようになります。基本的には以下のように使います。

  • view/procedure/function/packeageの(再)生成
  • バルクで参照データの再insert

単一のマイグレーション実行中では、ペンディング中のバージョン付きマイグレーションがすべて実行されたあと、リピータブルマイグレーションは常に最後に実行されます。リピータブルマイグレーションはその説明の順序で適用されます。

同一のリピータブルマイグレーションが複数回適用出来るように作成するのはプログラマの責任です。基本的にはDDLステートメントCREATE OR REPLACEを使います。

リピータブルマイグレーションの例は以下のようになります。

CREATE OR REPLACE VIEW blue_cars AS 
    SELECT id, license_plate FROM cars WHERE color='blue';

SQL-based migrations

マイグレーションは基本的にSQLで書きます。使い始めやすく、既存のスクリプト、ツールやスキルを流用できます。DBの全機能が使用可能で、中間の変換レイヤーを理解する手間を省けます。

SQLベースのマイグレーションは基本的には以下のように使います。

  • DDL(TABLE,VIEW,TRIGGER,SEQUENCEのCREATE/ALTER/DROPステートメント
  • 単純な参照データ変更(参照データテーブルのCRUD
  • 単純なバルクデータ変更(通常データテーブルのCRUD

Naming

Flywayにマイグレーションファイルを認識させるには、以下のネーミングルールに従う必要があります。

(ここにVersioned Migrations, Undo Migrations, Repeatable Migrationsの図がCSSを駆使して描かれている。本家ページ参照)

https://flywaydb.org/documentation/migrations#naming

ファイル名は以下のパーツで構成されます。

  • Prefix: バージョン付きの場合v変更), 取消の場合u変更)、リピータブルの場合R変更
  • Version: ドットもしくはアンダースコアでバージョンを記述(リピータブルでは不要)
  • Separator: __(アンダースコア2個)(変更
  • Description: 単語はアンダースコアかスペースで区切る。
  • Suffix: .sql変更

Discovery

FlywayはファイルシステムJavaクラスパス両方でSQLベースのマイグレーションを参照します。マイグレーションlocationsプロパティが指す一つ以上のディレクトリに置きます。

locationsにfilesystem:プレフィクスを付けるとファイルシステムをターゲットにします。プレフィクス無しかclasspath:を付けるとJavaのクラスパスをターゲットにします。

(ここにCSSを駆使した図解がある。下記URLの本家参照。)

https://flywaydb.org/documentation/migrations#discovery

新しいSQLベースのマイグレーションは実行時に自動的にファイルシステムJavaクラスパスのスキャンを行います。必要に応じてlocationsを設定する場合、Flywayは設定した命名規約にマッチする新規のSQLマイグレーションを自動的に検出します*2

スキャンは再帰的に行います。指定ディレクトリ下のすべてのディレクトリが対象となります。

Syntax

Flywayはすべての標準SQLをサポートします。これには以下を含みます。

  • 単一ハイフンもしくは複数行のステートメント
  • 単一ハイフン(en-dash)もしくは複数行(/* */)コメント
  • DB固有のSQLの文法拡張(PL/SQL, T-SQL)でストアドやパッケージの定義など

補足としてOracleの場合、FlywayはSQL*Plus commandsもサポートします。

Placeholder Replacement

標準SQLに加えて、Flywayはプレースホルダによる置換もサポートします。デフォルトでは${myplaceholder}のようなAntスタイルのプレールホルダになります。

環境間の差異を抽象化するのに役立ちます。

Example

サポートしているSQLの一例です。

/* Single line comment */
CREATE TABLE test_user (
  name VARCHAR(25) NOT NULL,
  PRIMARY KEY(name)
);

/*
Multi-line
comment
*/

-- Placeholder
INSERT INTO ${tableName} (name) VALUES ('Mr. T');

Java-based migrations

JavaベースのマイグレーションSQLでの表現が難しい変更をしたい場合に適しています。

基本的には以下のような場合に使います。

  • BLOB, CLOBの変更
  • 複雑なバルクデータの変更(再計算・複雑なフォーマット変更など)

Naming

Flywayに認識させるには、JavaベースのマイグレーションJdbcMigrationインタフェースを実装する必要があります。

デフォルトではFlywayはクラス名からバージョンと説明を自動抽出します。そのためには、クラス名は以下のネーミングルールに従う必要があります。

(ここにVersioned Migrations, Undo Migrations, Repeatable Migrationsの図がCSSを駆使して描かれている。本家ページ参照)

https://flywaydb.org/documentation/migrations#naming-1

ファイル名は以下のパーツで構成されます。

  • Prefix: バージョン付きの場合v, 取消の場合u、リピータブルの場合R
  • Version: アンダースコア(実行時に自動的にドットで置換される)で区切る。個数は任意(リピータブルでは不要)
  • Separator: __(アンダースコア2個)
  • Description: アンダースコア(実行時に自動的にスペースで置換される)で単語を区切る

クラス名にカスタマイズが必要な場合、MigrationInfoProviderインタフェースを実装してデフォルトの規約をオーバーライドします。

オーバーライドによりクラス名を任意に変更できます。バージョン・説明・マイグレーションカテゴリは対応するメソッドの実装で与えます。

Discovery

Flywayはlocationsプロパティが指すパッケージ内のJavaクラスパスにあるマイグレーションを参照します。

(ここにCSSを駆使した図解がある。下記URLの本家参照。)

https://flywaydb.org/documentation/migrations#discovery-1

新しいJavaベースのマイグレーションは実行時にクラスパスのスキャンで自動検出を行います。スキャンは再帰的に行います。あるパッケージのサブパッケージも対象になります。

Checksums and Validation

SQLマイグレーションとは異なり、Javaマイグレーションはデフォルトではチェックサムを持たないのでFlywayのvalidationによる変更検出の対象となりません。これはMigrationChecksumProviderインタフェースの実装により改良できます。getChecksum()メソッドで自前のチェックサムを実装します。これによりチェックサムが保存されてvalidationに使われます。

Sample Class

package db.migration;

import org.flywaydb.core.api.migration.jdbc.JdbcMigration;
import java.sql.Connection;
import java.sql.PreparedStatement;

/**
 * Example of a Java-based migration.
 */
public class V1_2__Another_user implements JdbcMigration {
    public void migrate(Connection connection) throws Exception {
        PreparedStatement statement =
            connection.prepareStatement("INSERT INTO test_user (name) VALUES ('Obelix')");

        try {
            statement.execute();
        } finally {
            statement.close();
        }
    }
}

Spring support (Optional)

Springの場合、SpringJdbcMigrationインタフェースを実装するという選択肢もあります。JdbcMigrationと同様な動作をしますが、素のJDBCではなくSpringのJdbcTemplateを使える点が異なります。

package db.migration;

import org.flywaydb.core.api.migration.spring.SpringJdbcMigration;
import org.springframework.jdbc.core.JdbcTemplate;

/**
 * Example of a Spring Jdbc migration.
 */
public class V1_2__Another_user implements SpringJdbcMigration {
    public void migrate(JdbcTemplate jdbcTemplate) throws Exception {
        jdbcTemplate.execute("INSERT INTO test_user (name) VALUES ('Obelix')");
    }
}

Transactions

デフォルトでは、Flywayは単一トランザクションマイグレーション実行を常にラップします。また、groupプロパティをtrueにすることで、単一トランザクション内にすべてのマイグレーション実行をラップできます*3

DBのなんらかの技術的制限が原因で、トランザクションで特定のステートメントを実行できないことをFlywayが検出する場合、トランザクションでそのマイグレーションは実行されません。その場合non-transactionalとマーキングされます。

デフォルトでは、トランザクショナルと非トランザクショナルのステートメントを単一マイグレーション実行に混ぜることは出来ません。ただし、mixedtrueにすることで可能になります。

Important Note

DBがcleanlyなトランザクションDDLステートメントをサポートする場合、失敗マイグレーションは常にロールバックします(ただしnon-transactionalのマーキングが無い場合に限る)。

一方、DBがcleanlyなトランザクションDDLステートメントをサポートしない場合(DDLステートメント前後に暗黙的なコミットを発行する場合など)、Flywayは失敗時にクリーン・ロールバックを実行出来ないので、マイグレーションを失敗とマーキングし、なんらかのマニュアルクリーンアップが必要なことを通知します。

Query Results

Flyway Pro

マイグレーションは第一義にリリースとデプロイ自動化プロセスの一部として実行されるので、SQLの結果を目で確認するケースは稀です。

とはいえ、そういう目視検査が意味を持つ場合も無くはないので、Flyway ProとEnterprise EditionではSELECT(と結果を返すその他のステートメント)実行時に表形式でクエリ結果を表示できます。

Schema History Table

どのマイグレーションがいつ誰によって適用されたかをトラッキングするために、Flywayはスキーマschema history tableを追加します。このテーブルはスキーマに対するすべての変更の実行記録と見なせます。また、マイグレーションチェックサムと個々のマイグレーションが成功したかどうかも持ちます。

使い方についてはhow Flyway worksのgetting started guideを読んで下さい。

Migration States

マイグレーションresolvedappliedのどちらかになります。ResolvedのマイグレーションはFlywayのファイルシステム・クラスパスのスキャナーが検出した 状態です。最初はpendingになります。DBに対して一度でも実行されると、appliedになります。

マイグレーションが成功するとFlywayのschema history tablesuccessになります。

マイグレーションが失敗し、かつ、DBがDDLトランザクションをサポートする場合、ロールバックしてschema history tableには何も記録されません。

マイグレーションが失敗し、かつ、DBがDDLトランザクションをサポートしない場合、schema history tableはfailedとなり、なんらかのマニュアルクリーンアップが必要かもしれないことを通知します。

取消マイグレーションでバージョン付きマイグレーションが取消された場合はundoneになります。

リピータブルマイグレーションが最後に適用された後にチェックサムが変更されている場合はoutdatedになり、再度実行するまでこのままになります。

既知の最も高いバージョンよりも高いappliedのバージョン付きマイグレーション(これは基本的にはソフトウェアの新バージョンが対象スキーマをマイグレートした場合に起きる)をFlywayが参照すると、そのマイグレーションfutureになります。

Callbacks

マイグレーションはおおよそのニーズを満たしますが、同一のアクションを何度も実行したい場合があります。プロシージャの再コンパイル、マテリアライズドビューの更新、様々なハウスキープ処理など。

そのため、Flywayはコールバックによりライフサイクルにフックをかけられます。

以下がFlywayがサポートするフックです。

Name Execution
beforeMigrate マイグレーション実行前
beforeEachMigrate マイグレーション実行中の各マイグレーション実行前
afterEachMigrate マイグレーション実行中の各マイグレーション実行後
afterMigrate マイグレーション実行後
beforeUndo Flyway Pro 取消マイグレーション実行前
beforeEachUndo Flyway Pro 取消マイグレーション中の各マイグレーション実行前
afterEachUndo Flyway Pro 取消マイグレーション中の各マイグレーション実行後
afterUndo Flyway Pro 取消マイグレーション実行後
beforeClean Clean実行前
afterClean Clean実行後
beforeInfo Info実行前
afterInfo Info実行後
beforeValidate Validate実行前
afterValidate Validate実行後
beforeBaseline Baseline実行前
afterBaseline Baseline実行後
beforeRepair Repair実行前
afterRepair Repair実行後

コールバックはSQLJavaのどちらかで実装します。

SQL Callbacks

Flywayのライフサイクルにフックをかける最も簡単な方法はSQLコールバックです。これは設定した場所に命名規約に従ってSQLファイルを単に置くもので、コールバック名の後ろにSQLマイグレーションサフィックス(※デフォルト.sql)を付けます。

デフォルト設定では、Flywayはデフォルトロケーション(CLIでは<install_dir>/sql)のbeforeMigrate.sql, beforeEachMigrate.sql, afterEachMigrate.sqlなどを参照します。

プレースホルダの置換はSQL migrationsと同様です。

Note: なおFlywayはSQLコールバックスキャンの際にsqlMigrationSuffixesの設定を優先します。

Java Callbacks

SQLコールバックが合わない場合、FlywayCallbackインターフェースを実装することもできます。ライフサイクルに複数のコールバック実装をフックさせることもできます。

More info: Java-based Callbacks

Error Handlers

Flyway Pro

FlywayのSQL実行はDBが返すすべての警告をレポートします。エラーが返される場合Flywayはすべての必要となる詳細を表示し、マイグレーションを失敗にして、可能であればロールバックを自動的に行います。

エラーは基本的に以下のように表示されます。

Migration V1__Create_person_table.sql failed
--------------------------------------------
SQL State  : 42001
Error Code : 42001
Message    : Syntax error in SQL statement "CREATE TABLE1[*] PERSON "; expected "OR, FORCE, VIEW, ...
Location   : V1__Create_person_table.sql (/flyway-tutorial/V1__Create_person_table.sql)
Line       : 1
Statement  : create table1 PERSON

デフォルトの振る舞いで基本的には充分です。

ただし、以下のようなことをしたい場合があります。

  • エラーが既知なので警告として扱い、後でしかるべき処置をしたい。
  • 警告をフェイルファーストにしたいのでエラーとして扱い、すぐに問題解決できるようにしたい。
  • DBが特定のエラーか警告を出す場合に特定の処理を紐づけたい。

Flyway ProとEnterprise Editionにはこれらを実現するためのエラーハンドラ(Error Handlers)があります。

Implementation

エラーハンドラはorg.flywaydb.core.api.errorhandler.ErrorHandlerインタフェースを実装するJavaクラスです。

このインターフェースにはメソッドが一つ(boolean handle(Context context))あり、DBが警告かエラーを少なくとも一つ返す場合に呼ばれます。インタフェース実装ではエラーや警告を見てそれに応じた処理をします。メッセージのログ出力や、例外のスローが出来ます。trueはエラーと警告が処理されたことを示します。falseはFlywayに次に設定されているErrorHandlerに進ませることを指示し、もし無い場合はデフォルトの振る舞いにフォールバックします。

Configuration

flyway.errorHandlersプロパティに複数のエラーハンドラを設定します。エラーハンドラは設定した順に呼び出されます。一つも設定されていない場合はFlywayはデフォルトの振る舞いにフォールバックします。

Example

Orcleのストアドプロシージャのコンパイルエラーは、デフォルトでは、ドライバは単に警告を出してFlywayはそれを出力します。

DB: Warning: execution completed with warning (SQL State: 99999 - Error Code: 17110)

以下は警告をエラーとして扱うエラーハンドラの例です。

package org.mycompany.mypkg;

import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.api.errorhandler.Context;
import org.flywaydb.core.api.errorhandler.ErrorHandler;
import org.flywaydb.core.api.errorhandler.Warning;

public class OracleProcedureFailFastErrorHandler implements ErrorHandler {
    @Override
    public boolean handle(Context context) {
        for (Warning warning : context.getWarnings()) {
            if ("99999".equals(warning.getState()) && warning.getCode() == 17110) {
                throw new FlywayException("Compilation failed");
            }
        }
        return false;
    }
}

Flywayには以下のように設定し、

flyway.errorHandlers=org.mycompany.mypkg.OracleProcedureFailFastErrorHandler

OrcleのストアドプロシージャのコンパイルエラーはCompilation failed即時エラー(immediate error)になります。

Dry Runs

Flyway Pro

FlywayがDBにマイグレーションを実行するときの動きは、適用対象のマイグレーションを参照し、ソートしてDBに対して直接それらを適用します。

デフォルトの振る舞いで基本的には充分です。

ただし、以下のようなことをしたい場合があります。

  • FlywayがDBにする予定の変更のプレビュー
  • 適用前にDBAにSQLのレビューを依頼
  • Flywayで更新されるものを確認し、実際のDB変更は別のツールでやる

Flyway ProとEnterprise Editionにはこれらを実現するためのDry Runsがあります。

Implementation

Dry Runの場合、Flywayはread-onlyでDB接続をセットアップします。Flywayはマイグレーション実行に必要なものを調べ、通常のマイグレーションで実行予定のすべてのステートメントを含む単一のSQLファイルを生成します。このSQLファイルをレビューします。これで十分な場合、FlywayにDBへのマイグレートを指示し、そうするとすべての変更が適用されます。もしくは、Flywayを使わず別のツールでDBに直接dry runのSQLファイルを適用することも出来ます。なお、このSQLファイルにはFlywayのschema history tableの生成と更新に必要なステートメントが含まれており、すべてのスキーマ変更は他同様にトラッキングされます。

この動作は透過的なので、その他のFlywayの機能である、SQLJavaマイグレーション、バージョン付き・リピータブル、コールバック、取消マイグレーションなどと一緒に使えます。

Configuration

Flywayのcommand-line tool, Maven plugin, Gradle pluginで使う場合、dry runの出力となるSQLファイルはflyway.dryRunOutputプロパティで設定します。

直接APIを使う場合、dry runの出力はjava.io.OutputStreamで設定します。CLIより細かい制御が出来ます。

プロパティを設定するとFlywayのdry runモードが有効になります。DBは変更されなくなり、適用予定のすべてのSQLステートメントがdry runの出力に送られるようになります。

*1:Repeatable migrations are applied in the order of their description.が原文。実際に試してないんだがdescriptionが順序になるだと思われ

*2:Flyway will automatically pick up any new SQL migrations as long as they conform to the configured naming convention.が原文。newてことはold(があるかどうかも分からんが)は自動検出対象外なんすかね?

*3:... the entire execution of all migrations of a single migration run within a single transaction ...が原文なんだが、なんか文章がヘンなような…… https://flywaydb.org/documentation/commandline/migrate#group を見ると「同一トランザクションにすべてのペンディング中のマイグレーションをグループ化する(DDLトランザクションをサポートするDBでのみ推奨)」と書いてある

Flywayのチュートリアル読んだ

flyway使ってるの初めて見て良く分からん感じだったのでとりあえずチュートリアル https://flywaydb.org/getstarted/ を読んで訳した。

※画像はすべて本家のものです。

Why database migrations?

まず最初にShinyというプロジェクトを仮定し、ここでの成果物の一つはShiny SoftというソフトウェアでこれはShiny DBというDBに接続します。

これを簡単に図示すると以下のようになります。

https://flywaydb.org/assets/balsamiq/SimpleView.png

他にもソフトウェアとDBが存在します。ここまでは良いでしょう。

しかし大半のプロジェクトにおいて、そのシンプルな世界観は急速に以下のように変貌します。

https://flywaydb.org/assets/balsamiq/Environments.png

環境のコピーを一回した程度では収まらず複数管理する必要が出てきます。このことは様々な課題を生み出します。

ソースコードの世界ではこれに上手く対処してきました。

  • 今日のバージョン管理は普遍的なツールになっています。
  • 繰り返し実行可能なビルドと継続的なインテグレーションが可能です。
  • 優れたリリースとデプロイ処理を行えます。

https://flywaydb.org/assets/balsamiq/SoftGreen.png

データベースの世界ではどうでしょうか?

https://flywaydb.org/assets/balsamiq/DbRed.png

今のところこれを上手くやることは出来ていません。たいていのプロジェクトでは未だに手動でSQLスクリプトを適用しています。時にはそれすらありません(問題を修正するためのその場限りのSQLが散在するなど)。ここで色々な疑問が浮かんできます。

  • あるマシン上のDBはどうなっているのか?
  • このスクリプトは適用済みなのか?そうでないのか?
  • 本番環境で流した緊急修正は後でテスト環境にも適用したのか?
  • データベースのインスタンスのセットアップ手順は?

基本的にはこれらの問いに対して我々は回答を持ち合わせていません。

データベースマイグレーションとはこうした混乱した状況の制御を取り戻すための有効な手段です。

これにより以下が可能となります。

  • クラッチからデータベースを再作成。
  • データベースの状態が常に明確となる。
  • DBの現行バージョンから新バージョンへ決定論的な方法で移行する。

How Flyway works

最もシンプルなシナリオでは空のDBにFlywayを立てる場合です。

https://flywaydb.org/assets/balsamiq/EmptyDb.png

Flywayはschema history tableを参照しようとします。データベースは空なのでFlywayは参照できず代わりに生成します。

デフォルトではflyway_schema_historyという一つの空のテーブルがDBに作られます。

https://flywaydb.org/assets/balsamiq/EmptySchemaVersion.png

このテーブルがDBの状態をトラッキングするのに使われます。

この後Flywayはファイルシステムかアプリケーションのクラスパスをマイグレーションのためにスキャンします。マイグレーションJavaSQLのどちらかで書きます。

マイグレーションバージョン番号ソートされており順番に適用されます。

https://flywaydb.org/assets/balsamiq/Migration-1-2.png

個々のマイグレーションが適用されるたびに、schema history tableもそれに応じて更新されます。

以下はflyway_schema_historyの例です。

installed_rank version description type script checksum installed_by installed_on execution_time success
1 1 Initial Setup SQL V1__Initial_Setup.sql 1996767037 axel 2016-02-04 22:23:00.0 546 true
2 2 First Changes SQL V2__First_Changes.sql 1279644856 axel 2016-02-06 09:18:00.0 127 true

メタデータと初期状態を配置し終えたので、次に新バージョンへのマイグレーションについて説明します。

Flywaryは再度ファイルシステムかアプリケーションのクラスパスをマイグレーションのためにスキャンします。マイグレーションはschema history tableとの照らし合わしを行います。バージョン番号が現行と等しいか低い場合、そのマイグレーションは無視されます。

それ以外のマイグレーションpending migrationsになります。これは、適用可能ではあるがまだ適用されていない、という意味です。

https://flywaydb.org/assets/balsamiq/PendingMigration.png

マイグレーションバージョン番号でソートされて順に実行されます。

https://flywaydb.org/assets/balsamiq/Migration21.png

schema history tableはそれに応じて更新されます。

flyway_schema_historyは以下のようになります。

installed_rank version description type script checksum installed_by installed_on execution_time success
1 1 Initial Setup SQL V1__Initial_Setup.sql 1996767037 axel 2016-02-04 22:23:00.0 546 true
2 2 First Changes SQL V2__First_Changes.sql 1279644856 axel 2016-02-06 09:18:00.0 127 true
3 2.1 Refactoring JDBC V2_1__Refactoring axel 2016-02-10 17:45:05.4 251 true

以上が一連の流れになります。データベースを更新する場合、DDLでもDMLでも、新規のマイグレーションを作成してバージョン番号を現行よりも高いものにします。それからFlywayを起動すると、そのマイグレーションでDBを更新します。

First Steps: Command-line

以下のカンタンなチュートリアルではFlywayのCLIでの使い方を解説します。設定方法と、最初のDBマイグレーションの書き方と動かし方が書いてあります。

このチュートリアルの所要時間はおおむね5分程度です。

Downloading and extracting Flyway

プラットフォームに応じたdownloading the Flyway Command-line ToolをDLして解凍します。

出来たディレクトリに移動します。

cd flyway-5.0.7

cd flyway-5.0.7

次に、以下のような/conf/flyway.confを編集してFlywayを設定します。

flyway.url=jdbc:h2:file:./foobardb
flyway.user=SA
flyway.password=

Creating the first migration

/sqlディレクトリにV1__Create_person_table.sqlという一つ目のマイグレーションを作成します。

create table PERSON (
    ID int not null,
    NAME varchar(100) not null
);

Migrating the database

DBをマイグレーションするためにFlywayを実行します。

flyway-5.0.7> flyway migrate

実行後、以下のような出力が得られます。

Database: jdbc:h2:file:./foobardb (H2 1.4)
Successfully validated 1 migration (execution time 00:00.008s)
Creating Schema History table: "PUBLIC"."flyway_schema_history"
Current version of schema "PUBLIC": << Empty Schema >>
Migrating schema "PUBLIC" to version 1 - Create person table
Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.033s)

Adding a second migration

次に、二つ目のマイグレーションV2__Add_people.sql/sqlディレクトリに追加します。

insert into PERSON (ID, NAME) values (1, 'Axel');
insert into PERSON (ID, NAME) values (2, 'Mr. Foo');
insert into PERSON (ID, NAME) values (3, 'Ms. Bar');

これを発行するためにFlywayを実行します。

flyway-5.0.7> flyway migrate

以下のような実行結果になります。

Database: jdbc:h2:file:./foobardb (H2 1.4)
Successfully validated 2 migrations (execution time 00:00.018s)
Current version of schema "PUBLIC": 1
Migrating schema "PUBLIC" to version 2 - Add people
Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.016s)

Summary

このチュートリアルでは以下について触れました。

マイグレーションが正常に実行されたのを確認できました。

First Steps: API

Prerequisites

Creating the project

以下のコマンドでMaven Archetype Pluginを使用してプロジェクトを生成します。

> mvn archetype:generate -B ^
        -DarchetypeGroupId=org.apache.maven.archetypes ^
        -DarchetypeArtifactId=maven-archetype-quickstart ^
        -DarchetypeVersion=1.1 ^
        -DgroupId=foo ^
        -DartifactId=bar ^
        -Dversion=1.0-SNAPSHOT ^
        -Dpackage=foobar

準備が出来たのでプロジェクトのディレクトリに移動します。

> cd bar

Adding the dependencies

pom.xmlにFlywayとH2を追加します。

<project ...>
    ...
    <dependencies>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
            <version>5.0.7</version>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>1.3.170</version>
        </dependency>
        ...
    </dependencies>
    ...
</project>

Integrating Flyway

src/main/java/foobar/App.javaでFlywayとの連携をするためにDBと接続するコードを書きます。

package foobar;

import org.flywaydb.core.Flyway;

public class App {
    public static void main(String[] args) {
        // Flywayインスタンスの生成
        Flyway flyway = new Flyway();

        // DB接続
        flyway.setDataSource("jdbc:h2:file:./target/foobar", "sa", null);

        // マイグレーション開始
        flyway.migrate();
    }
}

Creating the first migration

src/main/resources/db/migrationというマイグレーションディレクトリを作成します。以下のようにsrc/main/resources/db/migration/V1__Create_person_table.sqlという最初のマイグレーションを作ります。

create table PERSON (
    ID int not null,
    NAME varchar(100) not null
);

Executing our program

以下コマンドによりプログラムを実行します。

bar> mvn package exec:java -Dexec.mainClass=foobar.App

実行後、以下のような出力が得られます(タイムスタンプは消してあります)

INFO: Creating schema history table: "PUBLIC"."flyway_schema_history"
INFO: Current version of schema "PUBLIC": << Empty Schema >>
INFO: Migrating schema "PUBLIC" to version 1 - Create person table
INFO: Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.062s).

Adding a second migration

次のマイグレーションを追加するのにsrc/main/resources/db/migration/V2__Add_people.sqlを作ります。

insert into PERSON (ID, NAME) values (1, 'Axel');
insert into PERSON (ID, NAME) values (2, 'Mr. Foo');
insert into PERSON (ID, NAME) values (3, 'Ms. Bar');

以下で実行します。

bar> mvn package exec:java -Dexec.mainClass=foobar.App

以下のようになります。

INFO: Current version of schema "PUBLIC": 1
INFO: Migrating schema "PUBLIC" to version 2 - Add people
INFO: Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.090s).

このチュートリアルでは以下について解説しました。

  • プロジェクトとFlywayを連携させる
  • DBと接続するための設定
  • 一組のマイグレーションの作成

マイグレーションが正常に実行されたのを確認できました。

First Steps: Gradle

Prerequisites

  • Java 8 or 9
  • Gradle 3.0 or newer

Setting up the build file

FlywayとH2に接続する設定と連携するbuild.gradleを作成します。

buildscript {
    dependencies {
        classpath 'com.h2database:h2:1.4.191'
    }
}

plugins {
    id "org.flywaydb.flyway" version "5.0.7"
}

flyway {
    url = 'jdbc:h2:file:./target/foobar'
    user = 'sa'
}

Creating the first migration

src/main/resources/db/migration/V1__Create_person_table.sqlという最初のマイグレーションを作ります。

create table PERSON (
    ID int not null,
    NAME varchar(100) not null
);

Migrating the database

DBをマイグレートするためにFlywayを実行します。

> gradle flywayMigrate -i

実行後、以下のような出力になります。

Creating schema history table: "PUBLIC"."flyway_schema_history"
Current version of schema "PUBLIC": << Empty Schema >>
Migrating schema "PUBLIC" to version 1 - Create person table
Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.062s).

Adding a second migration

二つ目のマイグレーションsrc/main/resources/db/migration/V2__Add_people.sqlを追加します。

insert into PERSON (ID, NAME) values (1, 'Axel');
insert into PERSON (ID, NAME) values (2, 'Mr. Foo');
insert into PERSON (ID, NAME) values (3, 'Ms. Bar');

以下で実行します。

> gradle flywayMigrate -i

以下のような出力になります。

Current version of schema "PUBLIC": 1
Migrating schema "PUBLIC" to version 2 - Add people
Successfully applied 1 migration to schema "PUBLIC" (execution time 00:00.090s).

このチュートリアルでは以下について解説しました。

マイグレーションが正常に実行されたのを確認できました。

First Steps: Maven

(似たような内容なんで省略)