gh-ostとflywayについて
なぜgh-ostを使うのか/なぜflywayを使うのかは公式に任せる
解決したい問題
flywayでバージョン管理したいが、gh-ostを直接実行してスキーマ変更すると、その変更はflyway管理下に置けない
flyway migrate
したときにgh-ostコマンドが実行されて、成功/失敗がマークされるようにしたい
どうするのか
flywayの Custom Migration resolvers & executors を使って gh-ost
コマンドを実行できるようにする
公式は使い方が全く書いてないので、以下使い方を解説する
なお、この記事では flyway-core 6.1.1を使用
OSコマンドを実行することになるので自己責任で
Custom Migration resolvers & executors について
登場人物
- MigrationExecutor
- flyway migrateしたときの処理を定義するためのクラス
-
ResolvedMigration
に持たせることで、Flyway本体に実行させることができる - executeを実装すればよい
- ResolvedMigration
- 1つのmigrationのversionやdescription、
MigrationExecutor
などを定義するためのクラス -
getVersion
など実装すべきメソッドは多いが、いちばん大事なのはgetExecutor
- flyway infoしたときに表示される表の1行分の情報を持つと思えばいい。flyway infoの表ってのは↓みたいなやつ。
- 1つのmigrationのversionやdescription、
+-----------+---------+------------------------------------------+------+--------------+---------+
| Category | Version | Description | Type | Installed On | State |
+-----------+---------+------------------------------------------+------+--------------+---------+
| Versioned | 1.0 | init | SQL | | Pending |
| Versioned | 1.1 | add index for hoge | SQL | | Pending |
| Versioned | 1.2 | renamed unnecessary tables | SQL | | Pending |
| Versioned | 2.0 | drop unnecessary tables | SQL | | Pending |
+-----------+---------+------------------------------------------+------+--------------+---------+
- MigrationResolver
- ファイルなどを解決して
Collection<ResolvedMigration>
を返す - デフォではSQL-based MigrationsとJava-based Migrationsに必要なResolverが定義されている
-
resolveMigrations
を実装すればよい
- ファイルなどを解決して
実装
src/main/resources/ghost
に置いたファイルを読み込んでgh-ost使っていくケースを書いてみる
概要
- Maven Plugin や Gradle Plugin あたりを使ってflywayを実行できるようにしておく
- MigrationExecutorを実装したクラスを作成
- ResolvedMigrationを実装したクラスを作成
- MigrationResolverを実装したクラスを作成
- Resolverをconfで指定する
1. Maven Plugin や Gradle Plugin あたりを使ってflywayを実行できるようにしておく
とりあえず、ここではMaven Pluginの例を。別にMavenじゃなくてもいいです。
pom.xml
<project>
...
<dependencies>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>6.1.1</version>
<configuration>
<locations>
<location>classpath:db/migration</location>
</locations>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
2. MigrationExecutorを実装したクラスを作成
class GhostMigrationExecutor implements MigrationExecutor {
private final File file;
GhostMigrationExecutor(File file) {
this.file = file;
}
@Override
public void execute(Context context) throws SQLException {
// fileを読み込んで、ここで具体的なgh-ostのコマンドを実行する。ProcessBuilderなどを使うことになると思う。
}
@Override
public boolean canExecuteInTransaction() {
return false; //gh-ostはALTER TABLEしか扱えなくて、それはオートコミットなのでトランザクションに入れたらダメ。
}
}
3. ResolvedMigrationを実装したクラスを作成
class ResolvedGhostMigration implements ResolvedMigration {
private final File file;
private final MigrationVersion version;
private final String description;
ResolvedGhostMigration(File file) {
//この辺はFlywayオリジナルのBaseJavaMigrationがクラス名からversionとdescriptionを割り出すロジックを流用してる
boolean repeatable = file.getName().startsWith("R");
if (!file.getName().startsWith("V") && !repeatable) {
throw new FlywayException("Invalid file name: " + file.getName() + " => ensure it starts with V or R");
} else {
String prefix = file.getName().substring(0, 1);
Pair<MigrationVersion, String> info =
MigrationInfoHelper.extractVersionAndDescription(file.getName(), prefix, "__", new String[]{".properties"}, repeatable);
this.file = file;
this.version = info.getLeft();
this.description = info.getRight();
}
}
@Override
public MigrationVersion getVersion() {
return this.version;
}
@Override
public String getDescription() {
return this.description;
}
@Override
public String getScript() {
return this.file.getName();
}
@Override
public Integer getChecksum() {
// こいつがnullを返すのはオリジナルのResolvedJavaMigrationに準ずる。コンストラクタで渡されたfileから一意なchecksumを作ってnull以外を返してもよい。
// checksumを設定しておくと、すでにSuccessになったmigrationに利用したファイルをあとから編集したときにエラーを吐いてくれるようになる。
return null;
}
@Override
public MigrationType getType() {
// flyway infoしたときの `Type` カラムの値に使われる。Custom MigrationであることがわかるようにCUSTOMを返す
return MigrationType.CUSTOM;
}
@Override
public String getPhysicalLocation() {
return this.file.getAbsolutePath();
}
@Override
public MigrationExecutor getExecutor() {
return new GhostMigrationExecutor(this.file); //2で作成したExecutorを返すようにする
}
}
4. MigrationResolverを実装したクラスを作成
public class GhostMigrationResolver implements MigrationResolver {
@Override
public Collection<ResolvedMigration> resolveMigrations(Context context) {
try {
return Files.walk(Paths.get(getClass().getResource("/ghost/").toURI())) //src/main/resources/ghostディレクトリを読み込み
.map(Path::toFile)
.filter((file) -> !file.isDirectory()) // ghostディレクトリ自身を除外
.map(file -> new ResolvedGhostMigration(file)) // 3で作ったResolvedMigrationのインスタンスを生成
.collect(Collectors.toList());
} catch (IOException | URISyntaxException e) {
throw new FlywayException(e.getMessage(), e.getCause());
}
}
}
5. Resolverをconfで指定する
Mavenで作ったので、pom.xmlで指定できる
pom.xml
<project>
...
<dependencies>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.1.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>6.1.1</version>
<configuration>
<locations>
<location>classpath:db/migration</location>
</locations>
<!-- ここから -->
<resolvers>
<resolver>path.to.GhostMigrationResolver</resolver>
</resolvers>
<!-- ここまで追加 -->
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>
もしくは、flyway.confで指定してもよい
# ...その他の設定
flyway.resolvers=path.to.GhostMigrationResolver
注意
gh-ostとflywayは別々にコネクションを獲得することになり、gh-ostの実行中にflywayのコネクションがタイムアウトする可能性がある
実行する際は autoReconnect=true
にするなどの対策をしておいたほうがいい