Java
Vert.x
RxJava

Vert.x 3入門 〜 JDBC Service

More than 3 years have passed since last update.

Vert.xとは、公式ページより

JVM上にリアクティブアプリケーションを構築するためのツールキットです

@timfox氏が中心となって、現在2015年6月22日を目標にversion 3が開発されています。この投稿含め何回かに分けてVert.x 3の動くサンプルを実装していこうと思います。

概要

Vert.xは「Reactorパターン」というモデルを採用しており、そのため基本的にはブロッキングするような処理を記述すべきではありません。データベースに対する処理もこのブロッキングする処理にあてはまります。

公式の提供するVert.x JDBC Serviceを用いればデータベースに対する処理を非同期に実行することができ、ブロッキングしないで処理を記述することができます。

今回はVert.x JDBC Serviceを用いて、データをデータベースに永続化するシンプルなRESTサーバを動かしてみます。

前提

Vert.x 3はJava 8が必須となります、あらかじめJava 8をインストールしておいてください。Java 8がインストールできているかは以下コマンドで確認できます:

$ java -version
java version "1.8.0_45"
Java(TM) SE Runtime Environment (build 1.8.0_45-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.45-b02, mixed mode)

また、IntelliJを前提に解説しています、Eclipse等他のIDEの場合適宜読み替えてください。

データベースにはMySQLを用いています、PostgreSQL等他のデータベースを利用する場合適宜読み替えてください。
データベースのユーザはroot(パスワードもroot)を用いています、別のものを用いる場合適宜読み替えてください。

手順

データベースの準備

予めサンプルデータを格納するデータベースを作成しておきます、MySQLに接続して以下SQL文を実行します。

$ msql -u root -p
> create database vertx_example;

> create table products (
  id int not null auto_increment primary key,
  name varchar(20),
  price float,
  weight int
) engine=InnoDB;

> insert into products (name, price, weight) values
("Egg Whisk", 3.88, 150), ("Tea Cosy", 5.99, 100), ("Spatula", 1.00, 80);

ひな形のクローン

ひな形となるプロジェクトをクローンします。

$ git clone https://github.com/p-baleine/vertx-gradle-template.git database-simple
$ cd database-simple
$ git remote rm origin # origin削除

IntelliJでインポート

IntelliJを起動してWelcomeダイアログで[Import Project]を選択し、クローンした際にできたディレクトリを選択します。

Import Projectのダイアログで、[Import project from external model]にチェックを入れて、[Gradle]を選択し[Next]をクリックし、次に表示される画面で[Finish]をクリックします。

ServerVerticle

起点となるServerVerticleを実装します。

IntelliJでProjectヒエラルキーにてcom.example.helloworldパッケージを右クリックして[Refactor]→[Rename]を選択、com.example.databaseにリネームします。

IntelliJでProjectヒエラルキーにてcom.example.databaseパッケージ下のHelloWorldVerticleを右クリックして[Refactor]→[Rename]を選択、ServerVerticleにリネームします。

必要となるアーティファクトをbuild.gradleに追記します。

...
dependencies {
    compile 'io.vertx:vertx-core:3.0.0-milestone4'
    compile 'io.vertx:vertx-apex:3.0.0-milestone4' // 追加
    compile 'io.vertx:vertx-jdbc-service:3.0.0-milestone4' // 追加
    compile 'mysql:mysql-connector-java:5.1.35' // 追加
}
...

IntelliJで[View]→[Tool Windows]→[Gradle]を選択してGradle Projectsビューを表示したら、同期アイコンをクリックして依存関係を読み込みます。

com.example.database下のServerVerticleを開いて内容を以下の通り編集します。

package com.example.database;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.apex.Router;
import io.vertx.ext.apex.RoutingContext;
import io.vertx.ext.apex.handler.BodyHandler;
import io.vertx.ext.jdbc.JdbcService;
import io.vertx.ext.sql.SqlConnection;

import java.util.List;

public class ServerVerticle extends AbstractVerticle {

  @Override
  public void start() throws Exception {
    JsonObject config = new JsonObject().put("url", "jdbc:mysql://127.0.0.1:3306/vertx_example?user=root&password=root");
    DeploymentOptions options = new DeploymentOptions().setConfig(config);

    vertx.deployVerticle("service:io.vertx.jdbc-service", options, res -> {
      if (res.succeeded()) {
        Router router = Router.router(vertx);

        router.route().handler(BodyHandler.create());
        router.route().handler(routingContext -> {
          routingContext.response().putHeader("Content-Type", "application/json");
          routingContext.next();
        });

        router.get("/products").handler(this::handleListProduct);

        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
      } else {
        throw new RuntimeException(res.cause());
      }
    });
  }

  private void handleListProduct(RoutingContext routingContext) {
    JdbcService proxy = JdbcService.createEventBusProxy(vertx, "vertx.jdbc");

    proxy.getConnection(res -> {
      if (res.succeeded()) {
        SqlConnection connection = res.result();
        connection.query("select * from products", res2 -> {
          if (res2.succeeded()) {
            JsonArray arr = new JsonArray();
            List<JsonObject> result = res2.result().getRows();
            result.forEach(arr::add);
            connection.close(res3 -> {
              if (res3.succeeded()) {
                routingContext.response().end(arr.encode());
              } else {
                routingContext.fail(res3.cause());
              }
            });
          } else {
            connection.close(res3 -> {
              if (res3.succeeded()) {
                routingContext.fail(res2.cause());
              } else {
                routingContext.fail(res2.cause());
              }
            });
          }
        });
      } else {
        routingContext.fail(res.cause());
      }
    });
  }
}

アプリの起動

IntelliJの[Run]→[Edit Configurations...]を選択します。

Run/Debug Configurationsダイアログで[+]→[Application]を選択します。

以下の通り入力して、[OK]をクリックします:

  • Name: Database
  • Main classs: io.vertx.core.Starter
  • Program arguments: run com.example.database.ServerVerticle

IntelliJの[Run]→[Run 'Database']を選択します、IntelliJ上のコンソールで、エラーなく起動することを確認します

確認

以下コマンドを実行して、プロダクト一覧が返却されることを確認します:

$ curl -D - localhost:8080/products
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 123

[{"id":"1","name":"Egg Whisk","price":150},{"id":"2","name":"Tea Cosy","price":100},{"id":"3","name":"Spatula","price":30}]

コネクション取得処理の改善

データベースのコネクションの取得と引き続くSQL文の実行はどちらも非同期な処理となるため、一覧取得のハンドラhandleListProductsの処理は見通しが悪くなっています。

また、サンプルでは一覧取得のハンドラのみ実装していますが、今後詳細取得や新規追加等のハンドラを実装していく際に、コネクション取得部分の処理が冗長となります。

そのためコネクションの取得処理部分を共通のハンドラに切り出すよう改善したコードが以下です。

package com.example.database;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.impl.LoggerFactory;
import io.vertx.ext.apex.Router;
import io.vertx.ext.apex.RoutingContext;
import io.vertx.ext.apex.handler.BodyHandler;
import io.vertx.ext.jdbc.JdbcService;
import io.vertx.ext.sql.SqlConnection;

import java.util.List;

public class ServerVerticle extends AbstractVerticle {

  private final static Logger log = LoggerFactory.getLogger(ServerVerticle.class);
  private final static String CONNECTION_KEY = "connection";

  @Override
  public void start() throws Exception {
    JsonObject config = new JsonObject().put("url", "jdbc:mysql://127.0.0.1:3306/vertx_example?user=root");
    DeploymentOptions options = new DeploymentOptions().setConfig(config);

    vertx.deployVerticle("service:io.vertx.jdbc-service", options, res -> {
      if (res.succeeded()) {
        Router router = Router.router(vertx);

        router.route().handler(BodyHandler.create());
        router.route().handler(this::setupConnection);
        router.route().handler(routingContext -> {
          routingContext.response().putHeader("Content-Type", "application/json");
          routingContext.next();
        });

        router.get("/products").handler(this::handleListProduct);

        vertx.createHttpServer().requestHandler(router::accept).listen(8080);
      } else {
        throw new RuntimeException(res.cause());
      }
    });
  }

  private void setupConnection(RoutingContext routingContext) {
    JdbcService proxy = JdbcService.createEventBusProxy(vertx, "vertx.jdbc");
    proxy.getConnection(res -> {
      if (res.succeeded()) {
        routingContext.put(CONNECTION_KEY, res.result());
        routingContext.addHeadersEndHandler(v -> {
          SqlConnection connection = routingContext.get(CONNECTION_KEY);
          connection.close(c -> {
            if (!c.succeeded()) {
              log.error(c.cause());
            }
          });
        });
        routingContext.next();
      } else {
        routingContext.fail(res.cause());
      }
    });
  }

  private void handleListProduct(RoutingContext routingContext) {
    SqlConnection connection = routingContext.get(CONNECTION_KEY);

    connection.query("select * from products", res -> {
      if (res.succeeded()) {
        JsonArray arr = new JsonArray();
        List<JsonObject> result = res.result().getRows();
        result.forEach(arr::add);
        routingContext.response().end(arr.encode());
      } else {
        routingContext.fail(res.cause());
      }
    });
  }
}

解説

前述のとおりVert.xでは基本的にブロッキングするような処理を記述すべきではありません。ここで「基本的に」とは、Verticleがevent loop thread上で実行される場合のことを指します。

Vert.xでは特別な指定をしない限りVerticleはevent loop thread上で実行されます。今回のServerVerticleevent loop threadで実行されます。

Vert.xは、event loop thread上の処理がブロッキングしない限り、短い時間で大量の処理を実行することを保証します。このような実装はReactor Patterと呼ばれます。Node.jsはシングルスレッドで処理を実行するReactor Patternを採用しています。Vert.xでは複数のスレッドで処理を実行するためMulti Rector Patterと呼ばれます。

データベースのコネクションの取得やSQLの文の実行もブロッキングする処理です。JDBC Serviceを用いることでこれら処理を非同期に実行することができ、event loop threadでのブロッキングを避ける事ができます。

Vert.xではevent loop threadの他にworker threadも存在します。worker threadについては引き続く投稿でより詳しく解説します。

Vert.x 3では新しくサービスという概念が導入されました。今回のJDBC Serviceもこのサービスの一つです。サービスについては引き続く投稿でより詳しく解説します。

一覧表示以外を実装した完全なコードはこちらに置いてあります。

こちらもどうぞ