CLOVER🍀

That was when it all began.

QuarkusのReactive MySQL Clientを試す

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

Quarkusで、ReactiveなMySQLクライアントを使えるというので、試してみようかなと。

Quakus Reactive SQL Clients

Quarkusでデータベースアクセスを行う際にまず挙がってくるのはJPA(Hibernate)かなと思いますが、ReactiveなSQLクライアントも
備えています。

Quarkus - Reactive SQL Clients

1.8.1の時点だと、以下のデータベースに対応しているようです。

ドキュメントは、PostgreSQLを使って書かれています。

この機能は、SmallRye MunityとVert.xのReactive SQL Clientを使用して作成されています。

SmallRye Mutiny

Data access

https://github.com/smallrye/smallrye-reactive-utils/tree/1.1.0/vertx-mutiny-clients

Vert.xのReactive SQL Clientが、SmallRye Munityでラップされている感じですね。

今回は、この中のReactive MySQL Clientを使用します。JDBCドライバを使っているわけではないんですよねぇ…。

Reactive MySQL Client - Vert.x

https://github.com/smallrye/smallrye-reactive-utils/tree/1.1.0/vertx-mutiny-clients/vertx-mutiny-mysql-client

データベースごとに、Extensionおよび使用するクラスが異なります。

Database Clients details

では、試してみましょう。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.8 2020-07-14
OpenJDK Runtime Environment (build 11.0.8+10-post-Ubuntu-0ubuntu120.04)
OpenJDK 64-Bit Server VM (build 11.0.8+10-post-Ubuntu-0ubuntu120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.8, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-112-generic", arch: "amd64", family: "unix"

MySQLは8.0.21を使用し、172.17.0.2で動作しているものとします。

'mysql  Ver 8.0.21 for Linux on x86_64 (MySQL Community Server - GPL)' started.

プロジェクトの作成

最初に、プロジェクトを作りましょう。

書籍をお題に、Reactive MySQL Clientを使ってテーブルにアクセスし、JAX-RSで操作可能なアプリケーションを作成しましょう。

$ mvn io.quarkus:quarkus-maven-plugin:1.8.1.Final:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=resteasy-reactive-mysql \
    -Dextensions="resteasy-mutiny, resteasy-jsonb, reactive-mysql-client"

今回、特にポイントとなるのはreactive-mysql-clientですね。PostgreSQLやDB2の場合は、mysqlの部分を変更することになります。

作成したプロジェクトのディレクトリ内に移動。

$ cd resteasy-reactive-mysql

雛形

お題を書籍にするので、まずはテーブルを作成します。

mysql> create table book(
    ->      isbn varchar(14),
    ->      title varchar(255),
    ->      price int,
    ->      primary key(isbn)
    -> );
Query OK, 0 rows affected (0.10 sec)

Reactive SQL Clientにもスキーマ定義や初期データを登録するための方法はあるのですが、今回はパス。

Database schema and seed data

プロダクション環境ではFlywayを使った方が良いと書いていますし、このための処理を書くのもなぁ、と。

書籍クラス。JAX-RSリソースクラスで扱うものになります。
src/main/java/org/littlewings/quarkus/reactive/mysql/Book.java

package org.littlewings.quarkus.reactive.mysql;

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は省略
}

続いて、JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/reactive/mysql/BookResource.java

package org.littlewings.quarkus.reactive.mysql;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
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.RowIterator;
import io.vertx.mutiny.sqlclient.SqlClientHelper;
import io.vertx.mutiny.sqlclient.Tuple;

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

    // ここに処理を書く
}

主要な処理は、この後で記載していきます。

MySQLを操作するのは、こちらのMySQLPooLクラスを使用して行います。

    @Inject
    MySQLPool client;

クラスパス上に同じような名前のクラスがいくつかあるのですが、io.vertx.mutiny.mysqlclient.MySQLPoolを使用してください。

他のデータベースの場合は、利用するクラスも変更になります。

Database Clients details

データベースごとにクラスがあるわけですが、いずれもio.vertx.mutiny.sqlclient.Poolクラスを継承しているので…JDBCのように、
同じ型で扱えないものでしょうか…?

今回、ここの確認はパスします。

ちなみに、ドキュメントはPostgreSQLの例で記述されているので、MySQLの場合は以下あたりを参考にしました。

https://github.com/quarkusio/quarkus/tree/1.8.1.Final/integration-tests/reactive-mysql-client

https://github.com/eclipse-vertx/vertx-sql-client/blob/3.9.2/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLQueryTest.java

https://github.com/eclipse-vertx/vertx-sql-client/blob/3.9.2/vertx-mysql-client/src/test/java/io/vertx/mysqlclient/MySQLTransactionTest.java

設定

Reactive MySQL Clientの設定を行います。

今回は、このように設定。
src/main/resources/application.properties

# Configuration file
# key = value

quarkus.datasource.db-kind=mysql
quarkus.datasource.username=kazuhira
quarkus.datasource.password=password
quarkus.datasource.reactive.url=mysql://172.17.0.2:3306/practice

quarkus.datasource.reactive.mysql.charset=utf8mb4

Configuration Reference

quarkus.datasource.db-kindというのはデータベースの種類に応じた値を設定する必要があるので、今回はmysqlを指定。
MySQL以外のデータベースの場合の値は、こちらを参照。

https://github.com/quarkusio/quarkus/blob/1.8.1.Final/extensions/datasource/common/src/main/java/io/quarkus/datasource/common/runtime/DatabaseKind.java#L16-L22

その他の設定項目については、ドキュメントを参照してください。

Configuration Reference

ソースコードだと、このあたりを見るとよいでしょう。

https://github.com/quarkusio/quarkus/blob/1.8.1.Final/extensions/reactive-datasource/runtime/src/main/java/io/quarkus/reactive/datasource/runtime/DataSourceReactiveRuntimeConfig.java

https://github.com/quarkusio/quarkus/blob/1.8.1.Final/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/DataSourceReactiveMySQLConfig.java

https://github.com/quarkusio/quarkus/blob/1.8.1.Final/extensions/reactive-mysql-client/runtime/src/main/java/io/quarkus/reactive/mysql/client/runtime/MySQLPoolRecorder.java

https://github.com/eclipse-vertx/vertx-sql-client/blob/3.9.2/vertx-sql-client/src/main/java/io/vertx/sqlclient/PoolOptions.java#L34

データアクセスを行う処理を書く

では、データアクセスを行う処理を書いていきましょう。

このあたりを見ながら。

Using

MunityのUniやMultiを使って、処理を書いていくことになります。

検索系クエリ

まずは、シンプルなクエリを使った例を見つつ

Query results traversal

PreparedQuery(PreparedStatement)を使った例を見ていきます。

Prepared queries

?で、パラメーターをバインドするようです。

ドキュメントと違うのでは?と思うのですが、これは利用するSQL Clientによって差があるみたいですね。

Reactive MySQL Client / Prepared queries

Reactive PostgreSQL Client / Prepared queries

Quarkusのドキュメントは、PostgreSQLを使った例なので。

では、続けます。

パラメーターなしで、全件取得。

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> findAll() {
        return client
                .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> find(@PathParam("isbn") String isbn) {
        return client
                .preparedQuery("select isbn, title, price from book where isbn = ?")
                .execute(Tuple.of(isbn))
                .onItem()
                .transform(rows -> {
                    RowIterator<Row> iterator = rows.iterator();
                    Row row = iterator.next();

                    return Book.create(row.getString("isbn"), row.getString("title"), row.getInteger("price"));
                });
    }

MySQLPool#preparedQueryでクエリを作成し、PreparedQuery#executeにTupleを渡すことでパラメーターをバインドします。

        return client
                .preparedQuery("select isbn, title, price from book where isbn = ?")
                .execute(Tuple.of(isbn))

結果はRowSetになり、RowIteratorから結果を取得することができます。

                .transform(rows -> {
                    RowIterator<Row> iterator = rows.iterator();
                    Row row = iterator.next();

                    return Book.create(row.getString("isbn"), row.getString("title"), row.getInteger("price"));
                });
更新系クエリ

続いて、更新系のクエリを書いていきます。

トランザクションも使ってみましょう。

Transactions

SqlClientHelperを使った簡単なトランザクション管理と、Transactionを使った手動のトランザクション管理の2つの方法が
あります。

1件insert。

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<Book> put(Book book) {
        return SqlClientHelper
                .inTransactionUni(client, tx ->
                        tx
                                .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                                .execute(Tuple.of(book.getIsbn(), book.getTitle(), book.getPrice()))
                                .onItem()
                                .transform(rows -> book)
                );
    }

SqlClientHelper#inTransactionUniやSqlClientHelper#inTransactionMulti内で、トランザクションをコントロールします。

バッチ更新。

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> register(List<Book> books) {
        return SqlClientHelper
                .inTransactionMulti(client, tx -> {
                    List<Tuple> tuples =
                            books.stream().map(b -> Tuple.of(b.getIsbn(), b.getTitle(), b.getPrice())).collect(Collectors.toList());

                    return tx
                            .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                            .executeBatch(tuples)
                            .onItem()
                            .transformToMulti(rows -> Multi.createFrom().items(books.stream()));
                });
    }

PreparedQuery#executeBatchで、List<Tuple>を使ってバッチ更新を行うことができます。

                    List<Tuple> tuples =
                            books.stream().map(b -> Tuple.of(b.getIsbn(), b.getTitle(), b.getPrice())).collect(Collectors.toList());

                    return tx
                            .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                            .executeBatch(tuples)

トランザクション管理は、こちらはSqlClientHelper#inTransactionMultiを使用しています。

        return SqlClientHelper
                .inTransactionMulti(client, tx -> {

begin、commit、rollbackを使う方法も試してみましょう。

こちらは、ロールバックするようにしてあります。

    @POST
    @Path("manual")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> manualTransactionRegister(List<Book> books) {
        List<Tuple> tuples =
                books.stream().map(b -> Tuple.of(b.getIsbn(), b.getTitle(), b.getPrice())).collect(Collectors.toList());

        return client
                .begin()
                .onItem()
                .transformToMulti(tx ->
                        tx
                                .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                                .executeBatch(tuples)
                                .onItem()
                                .<Book>transformToMulti(v -> {
                                    throw new RuntimeException("oops!");  // -> rollback
                                })
                                // .transformToMulti(v1 -> tx.commit().onItem().transformToMulti(v2 -> Multi.createFrom().items(books.stream())))  // -> commit
                                .onFailure()
                                .recoverWithMulti(th ->
                                    tx.rollback().onItem().transformToMulti(v -> Multi.createFrom().failure(th))
                                )
                );
    }

こうすると、コミットするコードになります。

                                /*
                                .<Book>transformToMulti(v -> {
                                    throw new RuntimeException("oops!");  // -> rollback
                                })
                                */
                                .transformToMulti(v1 -> tx.commit().onItem().transformToMulti(v2 -> Multi.createFrom().items(books.stream())))  // -> commit

このAPIを使ったサンプルがなくて困ったのですが、SqlClientHelper#inTransaction〜の中で使われていたので、こちらを
参考にしました。

https://github.com/smallrye/smallrye-reactive-utils/blob/1.1.0/vertx-mutiny-clients/vertx-mutiny-sql-client/src/main/java/io/vertx/mutiny/sqlclient/SqlClientHelper.java#L13-L54

最後に、truncateも。

    @DELETE
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<Map<String, String>> truncate() {
        return client
                .preparedQuery("truncate table book")
                .execute()
                .onItem()
                .transform(rows -> Map.of("message", "OK"));
    }

こんな感じで。

確認

では、確認していきましょう。

ビルドして、起動。

$ mvn package
$ java -jar target/resteasy-reactive-mysql-1.0-SNAPSHOT-runner.jar

データを1件登録。

$ 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 200 OK
Content-Length: 90
Content-Type: application/json

{"isbn":"978-4798161488","price":4180,"title":"MySQL徹底入門 第4版 MySQL 8.0対応"}

複数件登録。

$ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/book -d '[
>   {
>     "isbn": "978-4621303252",
>     "title": "Effective Java 第3版",
>     "price": 4400
>   },
>   {
>     "isbn": "978-4295008477",
>     "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]",
>     "price": 2860
>   },
>   {
>     "isbn": "978-4798151120",
>     "title": "独習Java 新版",
>     "price": 3278
>   }
> ]'
HTTP/1.1 200 OK
Content-Length: 287
Content-Type: application/json

[{"isbn":"978-4621303252","price":4400,"title":"Effective Java 第3版"},{"isbn":"978-4295008477","price":2860,"title":"新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]"},{"isbn":"978-4798151120","price":3278,"title":"独習Java 新版"}]

1件取得。

$ curl -i localhost:8080/book/978-4798161488
HTTP/1.1 200 OK
Content-Length: 90
Content-Type: application/json

{"isbn":"978-4798161488","price":4180,"title":"MySQL徹底入門 第4版 MySQL 8.0対応"}

全件取得。

$ curl -s localhost:8080/book | jq
[
  {
    "isbn": "978-4621303252",
    "price": 4400,
    "title": "Effective Java 第3版"
  },
  {
    "isbn": "978-4798161488",
    "price": 4180,
    "title": "MySQL徹底入門 第4版 MySQL 8.0対応"
  },
  {
    "isbn": "978-4798151120",
    "price": 3278,
    "title": "独習Java 新版"
  },
  {
    "isbn": "978-4295008477",
    "price": 2860,
    "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]"
  }
]

ロールバックする例。

$ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/book/manual -d '[
>   {
>     "isbn": "978-4798147406",
>     "title": "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド",
>     "price": 3960
>   },
>   {
>     "isbn": "978-4873116389",
>     "title": "実践ハイパフォーマンスMySQL 第3版",
>     "price": 5280
>   }
> ]'
HTTP/1.1 500 Internal Server Error
content-length: 0

データは増えていません。

$ curl -s localhost:8080/book | jq
[
  {
    "isbn": "978-4621303252",
    "price": 4400,
    "title": "Effective Java 第3版"
  },
  {
    "isbn": "978-4798161488",
    "price": 4180,
    "title": "MySQL徹底入門 第4版 MySQL 8.0対応"
  },
  {
    "isbn": "978-4798151120",
    "price": 3278,
    "title": "独習Java 新版"
  },
  {
    "isbn": "978-4295008477",
    "price": 2860,
    "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]"
  }
]

ログはこんな感じになります。

2020-09-26 03:48:16,822 ERROR [org.jbo.res.res.i18n] (vert.x-eventloop-thread-14) RESTEASY002020: Unhandled asynchronous exception, sending back 500: java.lang.RuntimeException: oops!
    at org.littlewings.quarkus.reactive.mysql.BookResource.lambda$manualTransactionRegister$8(BookResource.java:107)
    at io.smallrye.mutiny.operators.UniOnItemTransformToMulti$FlatMapPublisherSubscriber.onItem(UniOnItemTransformToMulti.java:117)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.lambda$onItem$1(ContextPropagationUniInterceptor.java:35)
    at io.smallrye.context.SmallRyeThreadContext.lambda$withContext$0(SmallRyeThreadContext.java:217)
    at io.smallrye.mutiny.context.ContextPropagationUniInterceptor$1.onItem(ContextPropagationUniInterceptor.java:35)
    at io.smallrye.mutiny.operators.UniSerializedSubscriber.onItem(UniSerializedSubscriber.java:72)
    at io.smallrye.mutiny.vertx.AsyncResultUni.lambda$subscribing$1(AsyncResultUni.java:34)
    at io.vertx.mutiny.sqlclient.PreparedQuery$3.handle(PreparedQuery.java:161)
    at io.vertx.mutiny.sqlclient.PreparedQuery$3.handle(PreparedQuery.java:158)
    at io.vertx.sqlclient.impl.SqlResultHandler.complete(SqlResultHandler.java:97)
    at io.vertx.sqlclient.impl.SqlResultHandler.handle(SqlResultHandler.java:86)
    at io.vertx.sqlclient.impl.SqlResultHandler.handle(SqlResultHandler.java:33)
    at io.vertx.sqlclient.impl.TransactionImpl.lambda$wrap$2(TransactionImpl.java:139)

最後にtruncate。

$ curl -i -XDELETE localhost:8080/book
HTTP/1.1 200 OK
Content-Length: 16
Content-Type: application/json

{"message":"OK"}

0件になりました。

$ curl -s localhost:8080/book | jq
[]

まとめ

QuarkusのReactive MySQL Clientを試してみました。

リアクティブなSQLクライアントを使うのは初めてだったので、だいぶてこずりましたが、なんとかなりました…。

練習を繰り返さないと使いこなせない気がとてもとてもするのですが、Munity含め、少しずつ頑張ってみましょう。

最後に、作成したJAX-RSリソースクラス全体を載せておきます。
src/main/java/org/littlewings/quarkus/reactive/mysql/BookResource.java

package org.littlewings.quarkus.reactive.mysql;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
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.RowIterator;
import io.vertx.mutiny.sqlclient.SqlClientHelper;
import io.vertx.mutiny.sqlclient.Tuple;

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

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> findAll() {
        return client
                .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> find(@PathParam("isbn") String isbn) {
        return client
                .preparedQuery("select isbn, title, price from book where isbn = ?")
                .execute(Tuple.of(isbn))
                .onItem()
                .transform(rows -> {
                    RowIterator<Row> iterator = rows.iterator();
                    Row row = iterator.next();

                    return Book.create(row.getString("isbn"), row.getString("title"), row.getInteger("price"));
                });
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<Book> put(Book book) {
        return SqlClientHelper
                .inTransactionUni(client, tx ->
                        tx
                                .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                                .execute(Tuple.of(book.getIsbn(), book.getTitle(), book.getPrice()))
                                .onItem()
                                .transform(rows -> book)
                );
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> register(List<Book> books) {
        return SqlClientHelper
                .inTransactionMulti(client, tx -> {
                    List<Tuple> tuples =
                            books.stream().map(b -> Tuple.of(b.getIsbn(), b.getTitle(), b.getPrice())).collect(Collectors.toList());

                    return tx
                            .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                            .executeBatch(tuples)
                            .onItem()
                            .transformToMulti(rows -> Multi.createFrom().items(books.stream()));
                });
    }

    @POST
    @Path("manual")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Multi<Book> manualTransactionRegister(List<Book> books) {
        List<Tuple> tuples =
                books.stream().map(b -> Tuple.of(b.getIsbn(), b.getTitle(), b.getPrice())).collect(Collectors.toList());

        return client
                .begin()
                .onItem()
                .transformToMulti(tx ->
                        tx
                                .preparedQuery("insert into book(isbn, title, price) values(?, ?, ?)")
                                .executeBatch(tuples)
                                .onItem()
                                .<Book>transformToMulti(v -> {
                                    throw new RuntimeException("oops!");  // -> rollback
                                })
                                // .transformToMulti(v1 -> tx.commit().onItem().transformToMulti(v2 -> Multi.createFrom().items(books.stream())))  // -> commit
                                .onFailure()
                                .recoverWithMulti(th ->
                                    tx.rollback().onItem().transformToMulti(v -> Multi.createFrom().failure(th))
                                )
                );
    }

    @DELETE
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<Map<String, String>> truncate() {
        return client
                .preparedQuery("truncate table book")
                .execute()
                .onItem()
                .transform(rows -> Map.of("message", "OK"));
    }
}

Terraformのprovidersコマンドを試す

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

Terraformのprovidersというコマンドにちょっと興味があったので、調べてみようかと。

terraform providersコマンド

providersと言っているのは、terraformに含まれるコマンドです。

Command: providers - Terraform by HashiCorp

providersコマンドを使うと、現在の構成ファイルで使用しているTerraform Providerの情報を、依存関係を解析したうえで
表示してくれます。

さらにサブコマンドもあり、mirrorとschemaがあります。

Command: providers mirror - Terraform by HashiCorp

Command: providers schema - Terraform by HashiCorp

mirrorは、現在の構成に必要なProviderをダウンロードし、オフラインでも使えるようにする機能です。Terraformを利用する際に、
Providerを外部からダウンロードできない場合などに使用するようです。

こちらは、今回はパス。

schemaは、Providerが持っているリソースやデータソースに関するスキーマ情報を出力してくれます。こちらは、今回試して
みようと思います。

環境

今回の環境は、こちら。

$ terraform version
Terraform v0.13.3

この状態で、試していきます。

providersコマンドを試す

まずは、providersコマンドを試してみます。

Command: providers - Terraform by HashiCorp

こんな構成ファイルを用意。今回はConsul Providerを使用しました。
main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    consul = {
      source  = "hashicorp/consul"
      version = "2.10.0"
    }
  }
}

provider "consul" {
}

terraform initして

$ terraform init

確認すると、こんな感じに表示されます。

$ terraform providers

Providers required by configuration:
.
└── provider[registry.terraform.io/hashicorp/consul] 2.10.0

ソースコードを見ると、構成(Configuration)の他に、Stateに関するProviderも表示してくれるようです。

https://github.com/hashicorp/terraform/blob/v0.13.3/command/providers.go#L117-L125

もう少しバリエーションを試してみます。

2つProviderを使っている場合。Consul ProviderとMySQL Providerを使用。
main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    consul = {
      source  = "hashicorp/consul"
      version = "2.10.0"
    }

    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }
}

provider "consul" {
}

provider "mysql" {
}

terraform init後に実行すると、こんな感じになります。

$ terraform providers

Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/consul] 2.10.0
└── provider[registry.terraform.io/terraform-providers/mysql] 1.9.0

この表示内容は、versionの書き方に依存するようなので、たとえばこんな感じに変更すると

  required_providers {
    consul = {
      source  = "hashicorp/consul"
      version = ">= 2.10"
    }

    mysql = {
      source  = "terraform-providers/mysql"
      version = ">= 1.9"
    }
  }

terraform providersの結果にも反映されます。

$ terraform providers

Providers required by configuration:
.
├── provider[registry.terraform.io/hashicorp/consul] >= 2.10.*
└── provider[registry.terraform.io/terraform-providers/mysql] >= 1.9.*

最後に、backendも付けてみます。backendにはConsulを使用しました。
main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    consul = {
      source  = "hashicorp/consul"
      version = "2.10.0"
    }

    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }

  backend "consul" {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "state"
    lock    = true
  }
}

provider "consul" {
}

provider "mysql" {
  endpoint = "172.17.0.3:3306"
  username = "root"
  password = "password"
}

resource "mysql_database" "app" {
  name = "my_database"
}

ちょっと、リソース定義自体が増えていますが…。

まずはinitします。

$ terraform init

この時点では、Stateが使用しているProviderは実は現れません。

$ terraform providers

Providers required by configuration:
.
├── provider[registry.terraform.io/terraform-providers/mysql] 1.9.0
└── provider[registry.terraform.io/hashicorp/consul] 2.10.0

では、applyしてみます。

$ terraform apply

すると、State側のProviderも出現します。

$ terraform providers

Providers required by configuration:
.
├── provider[registry.terraform.io/terraform-providers/mysql] 1.9.0
└── provider[registry.terraform.io/hashicorp/consul] 2.10.0

Providers required by state:

    provider[registry.terraform.io/terraform-providers/mysql]

ですが、出現したのはMySQL Providerですね。

これって、もしかして「Stateに情報を保存しているProvider」の情報を表示しているんでしょうか?

そこで、構成ファイルを変更して、Consulに関するリソースも追加してみます。
main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    consul = {
      source  = "hashicorp/consul"
      version = "2.10.0"
    }

    mysql = {
      source  = "terraform-providers/mysql"
      version = "1.9.0"
    }
  }

  backend "consul" {
    address = "172.17.0.2:8500"
    scheme  = "http"
    path    = "state"
    lock    = true
  }
}

provider "consul" {
  address = "172.17.0.2:8500"
  scheme  = "http"
}

provider "mysql" {
  endpoint = "172.17.0.3:3306"
  username = "root"
  password = "password"
}

resource "consul_config_entry" "web" {
  name = "web"
  kind = "service-defaults"

  config_json = jsonencode({
    Protocol = "http"
  })
}

resource "mysql_database" "app" {
  name = "my_database"
}

terraform apply後に確認すると、このようになります。

$ terraform providers

Providers required by configuration:
.
├── provider[registry.terraform.io/terraform-providers/mysql] 1.9.0
└── provider[registry.terraform.io/hashicorp/consul] 2.10.0

Providers required by state:

    provider[registry.terraform.io/hashicorp/consul]

    provider[registry.terraform.io/terraform-providers/mysql]

やっぱり、Stateに情報を保存しているProviderが表示されているようですね。

そもそも、State自体のソースコードはTerraform本体に含まれているようなので、backendに選んだものが現れるわけではない、と。…。

https://github.com/hashicorp/terraform/tree/v0.13.3/backend/remote-state

このあたり、そのうちもう少しだけ追いたいところ。

providers schemaコマンド

次は、schemaコマンド。schemaコマンドを使用することで、構成ファイルで使用しているProviderのスキーマ情報を
表示してくれます。

Command: providers schema - Terraform by HashiCorp

たとえば、Consul Providerを使用するこんな構成ファイルがあったとして main.tf

terraform {
  required_version = "0.13.3"

  required_providers {
    consul = {
      source  = "hashicorp/consul"
      version = "2.10.0"
    }
  }
}

provider "consul" {
}

initします。

$ terraform init

その後、providers schemaコマンドを実行します。-jsonオプションが必要です。

$ terraform providers schema -json | jq

結果は、jqで整形しています。

{
  "format_version": "0.1",
  "provider_schemas": {
    "registry.terraform.io/hashicorp/consul": {
      "provider": {
        "version": 0,
        "block": {
          "attributes": {
            "address": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "ca_file": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "ca_path": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "ca_pem": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },

〜省略〜

            "token": {
              "type": "string",
              "description_kind": "plain",
              "optional": true,
              "sensitive": true
            }
          },
          "description_kind": "plain"
        }
      },
      "resource_schemas": {
        "consul_acl_auth_method": {
          "version": 0,
          "block": {
            "attributes": {
              "config": {
                "type": [
                  "map",
                  "string"
                ],
                "description": "The raw configuration for this ACL auth method.",
                "description_kind": "plain",
                "optional": true
              },
              "config_json": {
                "type": "string",
                "description": "The raw configuration for this ACL auth method.",
                "description_kind": "plain",
                "optional": true
              },
              "description": {
                "type": "string",
                "description": "A free form human readable description of the auth method.",
                "description_kind": "plain",
                "optional": true
              },
              "display_name": {
                "type": "string",
                "description": "An optional name to use instead of the name attribute when displaying information about this auth method.",
                "description_kind": "plain",
                "optional": true
              },
              "id": {
                "type": "string",
                "description_kind": "plain",
                "optional": true,
                "computed": true
              },

〜省略〜

      },
      "data_source_schemas": {
        "consul_acl_auth_method": {
          "version": 0,
          "block": {
            "attributes": {
              "config": {
                "type": [
                  "map",
                  "string"
                ],
                "description_kind": "plain",
                "computed": true
              },
              "config_json": {
                "type": "string",
                "description": "The raw configuration for this ACL auth method.",
                "description_kind": "plain",
                "computed": true
              },
              "description": {
                "type": "string",
                "description_kind": "plain",
                "computed": true
              },
              "display_name": {
                "type": "string",
                "description_kind": "plain",
                "computed": true
              },

〜省略〜

            },
            "description_kind": "plain"
          }
        }
      }
    }
  }
}

Provider自体のスキーマ情報、リソース、そしてデータソースのスキーマ情報を表示してくれます。

ブロックで定義する属性の情報は、こんな感じ。

            "block_types": {
              "namespace_rule": {
                "nesting_mode": "list",
                "block": {
                  "attributes": {
                    "bind_namespace": {
                      "type": "string",
                      "description_kind": "plain",
                      "required": true
                    },
                    "selector": {
                      "type": "string",
                      "description_kind": "plain",
                      "optional": true
                    }
                  },
                  "description_kind": "plain"
                }
              }
            },

ドキュメントにも記載があります。

Block Representation

また、ドキュメントではわからない、各属性のtypeなどがわかって便利なのですが、さすがにデフォルト値や
map等の属性に関してはわからないですね…。

              "config": {
                "type": [
                  "map",
                  "string"
                ],
                "description_kind": "plain",
                "required": true
              },

このあたりは、ドキュメントを頼るしかないでしょう。

このproviders schemaコマンドは、使用しているリソース等のスキーマ定義ではなく、依存しているProviderのスキーマ定義を
表示するので、使っていないリソース等のスキーマも表示してくれます。

ゆえに、大量に表示されるのですが…。

必要なものに絞って出力してもよいでしょう。

たとえば、consul_serviceリソースだけをjqコマンドで抜粋してみます。

consul_service

$ terraform providers schema -json | jq '.provider_schemas."registry.terraform.io/hashicorp/consul".resource_schemas.consul_service'
{
  "version": 0,
  "block": {
    "attributes": {
      "address": {
        "type": "string",
        "description_kind": "plain",
        "optional": true,
        "computed": true
      },
      "datacenter": {
        "type": "string",
        "description_kind": "plain",
        "optional": true,
        "computed": true
      },
      "enable_tag_override": {
        "type": "bool",
        "description_kind": "plain",
        "optional": true
      },
      "external": {
        "type": "bool",
        "description_kind": "plain",
        "optional": true
      },
      "id": {
        "type": "string",
        "description_kind": "plain",
        "optional": true,
        "computed": true
      },
      "meta": {
        "type": [
          "map",
          "string"
        ],
        "description_kind": "plain",
        "optional": true
      },
      "name": {
        "type": "string",
        "description_kind": "plain",
        "required": true
      },
      "namespace": {
        "type": "string",
        "description_kind": "plain",
        "optional": true
      },
      "node": {
        "type": "string",
        "description_kind": "plain",
        "required": true
      },
      "port": {
        "type": "number",
        "description_kind": "plain",
        "optional": true
      },
      "service_id": {
        "type": "string",
        "description_kind": "plain",
        "optional": true,
        "computed": true
      },
      "tags": {
        "type": [
          "list",
          "string"
        ],
        "description_kind": "plain",
        "optional": true
      }
    },
    "block_types": {
      "check": {
        "nesting_mode": "set",
        "block": {
          "attributes": {
            "check_id": {
              "type": "string",
              "description_kind": "plain",
              "required": true
            },
            "deregister_critical_service_after": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "http": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "interval": {
              "type": "string",
              "description_kind": "plain",
              "required": true
            },
            "method": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "name": {
              "type": "string",
              "description_kind": "plain",
              "required": true
            },
            "notes": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "status": {
              "type": "string",
              "description_kind": "plain",
              "optional": true,
              "computed": true
            },
            "tcp": {
              "type": "string",
              "description_kind": "plain",
              "optional": true
            },
            "timeout": {
              "type": "string",
              "description_kind": "plain",
              "required": true
            },
            "tls_skip_verify": {
              "type": "bool",
              "description_kind": "plain",
              "optional": true
            }
          },
          "block_types": {
            "header": {
              "nesting_mode": "set",
              "block": {
                "attributes": {
                  "name": {
                    "type": "string",
                    "description_kind": "plain",
                    "required": true
                  },
                  "value": {
                    "type": [
                      "list",
                      "string"
                    ],
                    "description_kind": "plain",
                    "required": true
                  }
                },
                "description_kind": "plain"
              }
            }
          },
          "description_kind": "plain"
        }
      }
    },
    "description_kind": "plain"
  }
}

スキーマに関する情報は、このあたりを参考に。

https://github.com/hashicorp/terraform/blob/v0.13.3/command/providers_schema.go

https://github.com/hashicorp/terraform/blob/v0.13.3/terraform/schemas.go

https://github.com/hashicorp/terraform/blob/v0.13.3/configs/configschema/schema.go