CLOVER🍀

That was when it all began.

Quarkus × Flywayを試す

これは、なにをしたくて書いたもの?

以前に、Flywayを試してみました。

データベースマイグレーションツール、Flywayを試してみる - CLOVER🍀

今回は、フレームワークに組み込んで使うパターンとしてQuarkusのExtensionがあるのでこちらを試してみました。

Quarkus - Using Flyway

Quarkus Flyway Extension

QuarkusのFlyway Extensionについてのドキュメントはこちら。

Quarkus - Using Flyway

実行方法は、以下の2つがあります。

  • quarkus.flyway.migrate-at-starttrueに設定し、アプリケーションの起動時にFlywayを自動実行する
  • Flywayインスタンスをインジェクションし、Flyway#migrateをプログラム内で呼び出して実行する

マイグレーションのデフォルトの配置先は、db/migrationです。

Flywayを使うために必要なExtensionはDatasource、というかJDBCです。

Quarkus - Datasources

あとは複数のデータソースを扱う方法や、Hibernateと組み合わせるといった話もあるようですが。

Schema Migration with Flyway / Multiple datasources

Schema Migration with Flyway / Flyway and Hibernate ORM

Using Hibernate ORM and JPA / Automatically transitioning to Flyway to Manage Schemas

今回は、以下の内容で扱ってみたいと思います。

  • Flywayはアプリケーションの起動時に自動実行し、単一のデータソースに適用する
  • 通常の起動とテストでの起動で、別々の設定にする
  • アプリケーションは、JDBCではなくReactive SQL Clientを使ってデータベースにアクセスする
  • 使用するデータベースは、MySQLとする

Quarkus - Reactive SQL Clients

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.1 2021-10-19
OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-96-generic", arch: "amd64", family: "unix"

MySQLのバージョンはこちらで、172.17.0.2で動作しているものとします。

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

プロジェクトを作成する

では、Quarkusプロジェクトを作成します。Flyway Extensionを使うのに最低限必要なのは、flyway、そしてjdbc-[使用するデータベース]です。

$ mvn io.quarkus.platform:quarkus-maven-plugin:2.6.2.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=flyway-migration \
    -DprojectVersion=0.0.1-SNAPSHOT \
    -Dextensions="resteasy-reactive,resteasy-reactive-jackson,flyway,jdbc-mysql,reactive-mysql-client"

その他は、今回のアプリケーションを書くのに選んだものです。

Extensionの表示。

[INFO] Looking for the newly published extensions in registry.quarkus.io
[INFO] -----------
[INFO] selected extensions: 
- io.quarkus:quarkus-jdbc-mysql
- io.quarkus:quarkus-flyway
- io.quarkus:quarkus-resteasy-reactive
- io.quarkus:quarkus-reactive-mysql-client
- io.quarkus:quarkus-resteasy-reactive-jackson

[INFO] 
applying codestarts...
[INFO] 📚  java
🔨  maven
📦  quarkus
📝  config-properties
🔧  dockerfiles
🔧  maven-wrapper
🚀  resteasy-reactive-codestart

プロジェクト内に移動。

$ cd flyway-migration

Maven依存関係は、こちら。

  <dependencies>                                                                                                                                                             
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-jdbc-mysql</artifactId>                                                                                                                            
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-flyway</artifactId>                                                                                                                                
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-resteasy-reactive</artifactId>                                                                                                                     
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-reactive-mysql-client</artifactId>                                                                                                                 
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-resteasy-reactive-jackson</artifactId>                                                                                                             
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-arc</artifactId>                                                                                                                                   
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.quarkus</groupId>                                                                                                                                          
      <artifactId>quarkus-junit5</artifactId>                                                                                                                                
      <scope>test</scope>                                                                                                                                                    
    </dependency>                                                                                                                                                            
    <dependency>                                                                                                                                                             
      <groupId>io.rest-assured</groupId>                                                                                                                                     
      <artifactId>rest-assured</artifactId>                                                                                                                                  
      <scope>test</scope>                                                                                                                                                    
    </dependency>                                                                                                                                                            
  </dependencies>

生成されたソースコードは、削除しておきます。

$ rm src/main/java/org/littlewings/* src/test/java/org/littlewings/*

あとはプログラムとマイグレーションを作成していきます。

マイグレーションとプログラムを書く

お題はFlywayなので、まずはマイグレーションを作りましょう。

src/main/resources/db/migration/V1__create_book_table.sql

create table book (
  isbn varchar(14),
  title varchar(255),
  price int,
  primary key(isbn)
);

書籍をテーマにしましょう。

こちらのテーブルに対して、Entity的なクラスと

src/main/java/org/littlewings/quarkus/flyway/Book.java

package org.littlewings.quarkus.flyway;

public class Book {
    String isbn;
    String title;
    int price;

    public static Book create(String isbn, String title, int price) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);

        return book;
    }

    // getter/setterは省略
}

RESTEasy Reactiveを使ったリソースクラスを作成。

src/main/java/org/littlewings/quarkus/flyway/BookResource.java

package org.littlewings.quarkus.flyway;

import java.net.URI;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
import io.vertx.mutiny.mysqlclient.MySQLPool;
import io.vertx.mutiny.sqlclient.Row;
import io.vertx.mutiny.sqlclient.RowSet;
import io.vertx.mutiny.sqlclient.Tuple;
import org.jboss.resteasy.reactive.RestPath;
import org.jboss.resteasy.reactive.RestResponse;

@Path("book")
public class BookResource {
    @Inject
    MySQLPool mysqlClient;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> findAll() {
        return mysqlClient
                .preparedQuery("select isbn, title, price from book order by price desc")
                .execute()
                .onItem()
                .transformToMulti(Multi.createFrom()::iterable)
                .onItem()
                .transform(row -> Book.create(row.getString("isbn"), row.getString("title"), row.getInteger("price")));
    }

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<Book> findOne(@RestPath String isbn) {
        return mysqlClient
                .preparedQuery("select isbn, title, price from book where isbn = ?")
                .execute(Tuple.of(isbn))
                .onItem()
                .transform(RowSet::iterator)
                .onItem()
                .transform(iterator -> {
                    if (iterator.hasNext()) {
                        Row row = iterator.next();
                        return Book.create(row.getString("isbn"), row.getString("title"), row.getInteger("price"));
                    } else {
                        return null;
                    }
                });
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<RestResponse<?>> put(Book book) {
        return mysqlClient
                .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                .execute(Tuple.of(book.getIsbn(), book.getTitle(), book.getPrice()))
                .onItem()
                .transform(rows -> RestResponse.created(URI.create("/book/" + book.isbn)));
    }

    @DELETE
    @Path("{isbn}")
    public Uni<Boolean> delete(@RestPath String isbn) {
        return mysqlClient
                .preparedQuery("delete from book where isbn = ?")
                .execute(Tuple.of(isbn))
                .onItem()
                .transform(rows -> rows.rowCount() == 1);
    }
}

アプリケーションの設定は、このようにしました。

src/main/resources/application.properties

# JDBC, Reactive SQL Client Common
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=kazuhira
quarkus.datasource.password=password

# JDBC
quarkus.datasource.jdbc.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_bin

# Flyway
quarkus.flyway.migrate-at-start=true

# Reactive SQL Client
quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/practice?characterEncoding=utf8mb4&charset=utf8mb4&collation=utf8mb4_bin

データベースの種類やユーザー名、パスワードはJDBC、Reactive SQL Clientを問わず同じ項目になります。

# JDBC, Reactive SQL Client Common
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=kazuhira
quarkus.datasource.password=password

JDBC接続URL。こちらは、今回はFlywayが使います。

# JDBC
quarkus.datasource.jdbc.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_bin

Reactive SQL Clientの場合はこちら。

# Reactive SQL Client
quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/practice?characterEncoding=utf8mb4&charset=utf8mb4&collation=utf8mb4_bin

Vert.x Reactive MySQL Clientの方が、より厳密にMySQLのCharsetとCollationを扱うようなのですが。

Reactive MySQL Client / Configuration

残念ながら、今回使おうと思ったCollation(utf8mb4_0900_bin)はサポートしていませんでした…。

https://github.com/eclipse-vertx/vertx-sql-client/blob/4.2.2/vertx-mysql-client/src/main/java/io/vertx/mysqlclient/impl/MySQLCollation.java

なので、utf8mb4_binを指定しています。

指定しないと、Vert.x Reactive MySQL ClientのデフォルトのCharsetとCollationになるみたいなんですよね。

// set connection collation to utf8_general_ci instead of the default collation utf8mb4_general_ci

ちなみに、ドキュメントを見るとURLで指定できる項目はやや少なめに見えるのですが

Currently, the client supports the following parameter keywords in connection uri (keys are case-insensitive):

実際はもっと指定できそうです。

https://github.com/eclipse-vertx/vertx-sql-client/blob/4.2.2/vertx-mysql-client/src/main/generated/io/vertx/mysqlclient/MySQLConnectOptionsConverter.java

ムリにcollation=utf8mb4_0900_binと指定すると、例外がスローされます。

Caused by: java.lang.IllegalArgumentException: Unsupported collation: utf8mb4_0900_bin
    at io.vertx.mysqlclient.MySQLConnectOptions.setCollation(MySQLConnectOptions.java:137)
    at io.vertx.mysqlclient.MySQLConnectOptionsConverter.fromJson(MySQLConnectOptionsConverter.java:40)
    at io.vertx.mysqlclient.MySQLConnectOptions.<init>(MySQLConnectOptions.java:90)
    at io.vertx.mysqlclient.MySQLConnectOptions.fromUri(MySQLConnectOptions.java:55)
    at io.quarkus.reactive.mysql.client.runtime.MySQLPoolRecorder.toMySQLConnectOptions(MySQLPoolRecorder.java:100)
    at io.quarkus.reactive.mysql.client.runtime.MySQLPoolRecorder.initialize(MySQLPoolRecorder.java:63)
    at io.quarkus.reactive.mysql.client.runtime.MySQLPoolRecorder.configureMySQLPool(MySQLPoolRecorder.java:45)
    at io.quarkus.deployment.steps.ReactiveMySQLClientProcessor$build360423335.deploy_0(Unknown Source)
    at io.quarkus.deployment.steps.ReactiveMySQLClientProcessor$build360423335.deploy(Unknown Source)
    ... 51 more

ちょっと余談でした。

Flywayの設定は、今回はこれだけです。

# Flyway
quarkus.flyway.migrate-at-start=true

これで、アプリケーションの起動時にFlywayが実行されます。

動作確認してみる

それでは、動作確認してみましょう。

まだこの時点では、対象のデータベースにはなにもありません。

mysql> use practice;
Database changed
mysql> show tables;
Empty set (0.01 sec)

パッケージングして

$ mvn package

起動。

$ java -jar target/quarkus-app/quarkus-run.jar

自動するとすぐにマイグレーションが適用されます。

2022-01-21 01:33:24,502 INFO  [org.fly.cor.int.lic.VersionPrinter] (main) Flyway Community Edition 8.1.0 by Redgate
2022-01-21 01:33:24,520 INFO  [org.fly.cor.int.dat.bas.BaseDatabaseType] (main) Database: jdbc:mysql://172.17.0.2:3306/practice (MySQL 8.0)
2022-01-21 01:33:24,624 INFO  [org.fly.cor.int.sch.JdbcTableSchemaHistory] (main) Creating Schema History table `practice`.`flyway_schema_history` ...
2022-01-21 01:33:25,120 INFO  [org.fly.cor.int.com.DbMigrate] (main) Current version of schema `practice`: << Empty Schema >>
2022-01-21 01:33:25,140 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema `practice` to version "1 - create book table"
2022-01-21 01:33:25,420 INFO  [org.fly.cor.int.com.DbMigrate] (main) Successfully applied 1 migration to schema `practice`, now at version v1 (execution time 00:00.322s)

適用されたExtension。

2022-01-21 01:33:25,719 INFO  [io.quarkus] (main) Installed features: [agroal, cdi, flyway, jdbc-mysql, narayana-jta, reactive-mysql-client, resteasy-reactive, resteasy-reactive-jackson, smallrye-context-propagation, vertx]

データベースを確認すると、テーブルが追加され、マイグレーションの履歴も登録されています。

mysql> show tables;
+-----------------------+
| Tables_in_practice    |
+-----------------------+
| book                  |
| flyway_schema_history |
+-----------------------+
2 rows in set (0.00 sec)

mysql> select * from flyway_schema_history;
+----------------+---------+-------------------+------+----------------------------------------+-----------+--------------+---------------------+----------------+---------+
| installed_rank | version | description       | type | script                                 | checksum  | installed_by | installed_on        | execution_time | success |
+----------------+---------+-------------------+------+----------------------------------------+-----------+--------------+---------------------+----------------+---------+
|              1 | 1       | create book table | SQL  | db/migration/V1__create_book_table.sql | 657681960 | kazuhira     | 2022-01-20 16:33:25 |            208 |       1 |
+----------------+---------+-------------------+------+----------------------------------------+-----------+--------------+---------------------+----------------+---------+
1 row in set (0.00 sec)

データを登録して動作確認。

$ curl -i -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4798161488 -d '{
  "isbn": "978-4798161488",
  "title": "MySQL徹底入門 第4版 MySQL 8.0対応",
  "price": 4180
}'
HTTP/1.1 201 Created
Content-Type: application/json
Location: /book/978-4798161488
content-length: 0


$ curl -i -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4798147406 -d '{
  "isbn": "978-4798147406",
  "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
  "price": 3960
}'
HTTP/1.1 201 Created
Content-Type: application/json
Location: /book/978-4798147406
content-length: 0


$ curl -i -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4295000297 -d '{
  "isbn": "978-4295000297",
  "title": "MySQL 即効クエリチューニング",
  "price": 1980
}'
HTTP/1.1 201 Created
Content-Type: application/json
Location: /book/978-4295000297
content-length: 0

データを見てみます。

$ curl -s localhost:8080/book | jq
[
  {
    "isbn": "978-4798161488",
    "title": "MySQL徹底入門 第4版 MySQL 8.0対応",
    "price": 4180
  },
  {
    "isbn": "978-4798147406",
    "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
    "price": 3960
  },
  {
    "isbn": "978-4295000297",
    "title": "MySQL 即効クエリチューニング",
    "price": 1980
  }
]


$ curl -s localhost:8080/book/978-4798147406 | jq
{
  "isbn": "978-4798147406",
  "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
  "price": 3960
}

削除。

$ curl -i -XDELETE localhost:8080/book/978-4295000297
HTTP/1.1 200 OK
content-length: 4
Content-Type: text/plain;charset=UTF-8

確認。

$ curl -s localhost:8080/book | jq
[
  {
    "isbn": "978-4798161488",
    "title": "MySQL徹底入門 第4版 MySQL 8.0対応",
    "price": 4180
  },
  {
    "isbn": "978-4798147406",
    "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
    "price": 3960
  }
]

OKですね。

1度ここでアプリケーションを終了させます。

マイグレーションを追加してみる

もうひとつ、マイグレーションを追加してみましょう。

src/main/resources/db/migration/V2__create_account_table.sql

create table account (
  id int,
  name varchar(50),
  registered datetime,
  about varchar(255),
  primary key(id)
);

パッケージングして

$ mvn package

実行。

$ java -jar target/quarkus-app/quarkus-run.jar

起動時に、新しいマイグレーションが適用されました。

2022-01-21 01:41:07,579 INFO  [org.fly.cor.int.lic.VersionPrinter] (main) Flyway Community Edition 8.1.0 by Redgate
2022-01-21 01:41:07,595 INFO  [org.fly.cor.int.dat.bas.BaseDatabaseType] (main) Database: jdbc:mysql://172.17.0.2:3306/practice (MySQL 8.0)
2022-01-21 01:41:07,707 INFO  [org.fly.cor.int.com.DbMigrate] (main) Current version of schema `practice`: 1
2022-01-21 01:41:07,732 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema `practice` to version "2 - create account table"
2022-01-21 01:41:07,949 INFO  [org.fly.cor.int.com.DbMigrate] (main) Successfully applied 1 migration to schema `practice`, now at version v2 (execution time 00:00.277s)

テーブルと履歴の確認。

mysql> show tables;
+-----------------------+
| Tables_in_practice    |
+-----------------------+
| account               |
| book                  |
| flyway_schema_history |
+-----------------------+
3 rows in set (0.00 sec)

mysql> select * from flyway_schema_history;
+----------------+---------+----------------------+------+-------------------------------------------+------------+--------------+---------------------+----------------+---------+
| installed_rank | version | description          | type | script                                    | checksum   | installed_by | installed_on        | execution_time | success |
+----------------+---------+----------------------+------+-------------------------------------------+------------+--------------+---------------------+----------------+---------+
|              1 | 1       | create book table    | SQL  | db/migration/V1__create_book_table.sql    |  657681960 | kazuhira     | 2022-01-20 16:33:25 |            208 |       1 |
|              2 | 2       | create account table | SQL  | db/migration/V2__create_account_table.sql | 1435889679 | kazuhira     | 2022-01-20 16:41:07 |            130 |       1 |
+----------------+---------+----------------------+------+-------------------------------------------+------------+--------------+---------------------+----------------+---------+
2 rows in set (0.00 sec)

OKですね。

この確認は、ここまでにしておきます。

テストで使ってみる

最後に、テストでも使ってみたいと思います。

まず、テスト時と通常のアプリケーションの実行時で、設定を分けてみます。

src/main/resources/application.properties

# JDBC, Reactive SQL Client Common
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=kazuhira
quarkus.datasource.password=password

# JDBC
quarkus.datasource.jdbc.url=jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_bin

# Flyway
quarkus.flyway.migrate-at-start=true

# Reactive SQL Client
quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/practice?characterEncoding=utf8mb4&charset=utf8mb4&collation=utf8mb4_bin


## test
%test.quarkus.datasource.username=testuser
%test.quarkus.datasource.password=password
%test.quarkus.datasource.jdbc.url=jdbc:mysql://172.17.0.2:3306/test?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_bin
%test.quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/test?characterEncoding=utf8mb4&charset=utf8mb4&collation=utf8mb4_bin

%test.quarkus.flyway.locations=db/migration,test/db/migration

%testというのは、test Profile時に有効になる設定ですね。

Configuration Reference / Profiles

接続先は、テスト用にここまでに出てきた項目を定義しているのですが

%test.quarkus.datasource.username=testuser
%test.quarkus.datasource.password=password
%test.quarkus.datasource.jdbc.url=jdbc:mysql://172.17.0.2:3306/test?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_bin
%test.quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/test?characterEncoding=utf8mb4&charset=utf8mb4&collation=utf8mb4_bin

今回は、さらにFlyway用の設定を追加しました。

%test.quarkus.flyway.locations=db/migration,test/db/migration

この設定で、db/migrationtest/db/migrationの2つのクラスパス上のマイグレーションが対象になります。

テスト用のマイグレーションとして、初期データ登録を入れてみました。

src/test/resources/test/db/migration/V99__insert_test_data.sql

insert into book(isbn, title, price) values('978-4798161488', 'MySQL徹底入門 第4版 MySQL 8.0対応', 4180);

また、以下の設定はProfile共通で有効になるので、テスト時にもFlywayは自動実行されます。

# Flyway
quarkus.flyway.migrate-at-start=true

作成したテストコードは、こちら。

src/test/java/org/littlewings/quarkus/flyway/BookResourceTest.java

package org.littlewings.quarkus.flyway;

import java.util.List;

import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;

@QuarkusTest
@TestHTTPEndpoint(BookResource.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BookResourceTest {
    @Test
    @Order(1)
    public void initialFindAll() {
        given()
                .when()
                .get()
                .then()
                .statusCode(200)
                .body("$", hasSize(1))
                .body("isbn", hasItem("978-4798161488"))
                .body("title", hasItem("MySQL徹底入門 第4版 MySQL 8.0対応"))
                .body("price", hasItem(4180));
    }

    @Test
    @Order(2)
    public void putBooks() {
        List<Book> books = List.of(
                Book.create("978-4621303252", "Effective Java 第3版", 4400),
                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278),
                Book.create("978-4295010333", "いちばんやさしいJavaの教本 人気講師が教えるプログラミングの基礎", 2640)
        );

        books.forEach(book -> {
            given()
                    .contentType(ContentType.JSON)
                    .body(book)
                    .when()
                    .put("/" + book.getIsbn())
                    .then()
                    .statusCode(201)
                    .header("Location", "/book/" + book.getIsbn());
        });

        given()
                .when()
                .get()
                .then()
                .statusCode(200)
                .body("$", hasSize(4));
    }

    @Test
    @Order(3)
    public void findOne() {
        given()
                .when()
                .get("/978-4774189093")
                .then()
                .statusCode(200)
                .body("isbn", is("978-4774189093"))
                .body("title", is("Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで"))
                .body("price", is(3278));
    }

    @Test
    @Order(4)
    public void deleteBooks() {
        List<Book> books = List.of(
                Book.create("978-4621303252", "Effective Java 第3版", 4400),
                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278),
                Book.create("978-4295010333", "いちばんやさしいJavaの教本 人気講師が教えるプログラミングの基礎", 2640)
        );

        books.forEach(book -> {
            when()
                    .delete("/" + book.getIsbn())
                    .then()
                    .statusCode(200);
        });

        given()
                .when()
                .get()
                .then()
                .statusCode(200)
                .body("$", hasSize(1));
    }
}

テスト前のデータベースの状態を確認。

mysql> use test;
Database changed
mysql> show tables;
Empty set (0.01 sec)

では、テストを実行します。

$ mvn test

テスト実行時にマイグレーションが適用されていることが確認できます。

[INFO] Running org.littlewings.quarkus.flyway.BookResourceTest
2022-01-21 01:53:20,698 INFO  [org.fly.cor.int.lic.VersionPrinter] (main) Flyway Community Edition 8.1.0 by Redgate
2022-01-21 01:53:20,706 INFO  [org.fly.cor.int.dat.bas.BaseDatabaseType] (main) Database: jdbc:mysql://172.17.0.2:3306/test (MySQL 8.0)
2022-01-21 01:53:20,758 INFO  [org.fly.cor.int.sch.JdbcTableSchemaHistory] (main) Creating Schema History table `test`.`flyway_schema_history` ...
2022-01-21 01:53:20,995 INFO  [org.fly.cor.int.com.DbMigrate] (main) Current version of schema `test`: << Empty Schema >>
2022-01-21 01:53:21,040 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema `test` to version "1 - create book table"
2022-01-21 01:53:21,411 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema `test` to version "2 - create account table"
2022-01-21 01:53:21,539 INFO  [org.fly.cor.int.com.DbMigrate] (main) Migrating schema `test` to version "99 - insert test data"
2022-01-21 01:53:21,573 INFO  [org.fly.cor.int.com.DbMigrate] (main) Successfully applied 3 migrations to schema `test`, now at version v99 (execution time 00:00.594s)

テスト終了後、データベース側を確認。

mysql> show tables;
+-----------------------+
| Tables_in_test        |
+-----------------------+
| account               |
| book                  |
| flyway_schema_history |
+-----------------------+
3 rows in set (0.01 sec)

mysql> select * from flyway_schema_history;
+----------------+---------+----------------------+------+---------------------------------------------+------------+--------------+---------------------+----------------+---------+
| installed_rank | version | description          | type | script                                      | checksum   | installed_by | installed_on        | execution_time | success |
+----------------+---------+----------------------+------+---------------------------------------------+------------+--------------+---------------------+----------------+---------+
|              1 | 1       | create book table    | SQL  | db/migration/V1__create_book_table.sql      |  657681960 | testuser     | 2022-01-20 16:53:21 |            332 |       1 |
|              2 | 2       | create account table | SQL  | db/migration/V2__create_account_table.sql   | 1435889679 | testuser     | 2022-01-20 16:53:21 |             85 |       1 |
|              3 | 99      | insert test data     | SQL  | test/db/migration/V99__insert_test_data.sql |  905141387 | testuser     | 2022-01-20 16:53:21 |              4 |       1 |
+----------------+---------+----------------------+------+---------------------------------------------+------------+--------------+---------------------+----------------+---------+
3 rows in set (0.00 sec)

2つのディレクトリのマイグレーションが適用されていることが確認できました。

まとめ

今回はFlywayをQuarkusに組み込んで(Flyway Extension)を使って試してみました。

簡単に使えて良いですね。Flyway自体は前に試していたので、そんなに困りませんでした。