Vert.x 3入門 〜 JDBC Service

[Vert.x](http://vert-x3.github.io/)とは、公式ページより

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


[@timfox](https://twitter.com/timfox)氏が中心となって、[現在2015年6月22日を目標](https://github.com/vert-x3/wiki/wiki/Vert.x-Roadmap#vertx-300-final-release-22-june-2015)にversion 3が開発されています。この投稿含め何回かに分けてVert.x 3の動くサンプルを実装していこうと思います。

# 概要

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

公式の提供する[Vert.x JDBC Service](https://github.com/vert-x3/vertx-jdbc-client)を用いればデータベースに対する処理を非同期に実行することができ、ブロッキングしないで処理を記述することができます。

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

# 前提

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

```bash
$ 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文を実行します。

```bash
$ 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);
```

## ひな形のクローン

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

```bash
$ 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に追記します。

```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`を開いて内容を以下の通り編集します。

```java
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上のコンソールで、エラーなく起動することを確認します

### 確認

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

```bash
$ 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`の処理は見通しが悪くなっています。

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

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

```java
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`上で実行されます。今回の`ServerVerticle`も`event loop thread`で実行されます。

Vert.xは、`event loop thread`上の処理がブロッキングしない限り、短い時間で大量の処理を実行することを保証します。このような実装は[Reactor Patter](http://en.wikipedia.org/wiki/Reactor_pattern)と呼ばれます。[Node.js](https://nodejs.org/)はシングルスレッドで処理を実行するReactor Patternを採用しています。Vert.xでは複数のスレッドで処理を実行するため[Multi Rector Patter](http://vert-x3.github.io/docs/vertx-core/java/#_reactor_and_multi_reactor)と呼ばれます。

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

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

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

一覧表示以外を実装した完全なコードは[こちら](https://github.com/p-baleine/step-by-step-vert.x/tree/master/examples/database)に置いてあります。


# こちらもどうぞ

* [Vert.x 3入門 〜 HelloWorldVerticle](http://qiita.com/p-baleine@github/items/304912bea4a041ae653b)
* [Vert.x 3入門 〜 Webアプリケーションフレームワーク Apex](http://qiita.com/p-baleine@github/items/d9975dad4e44987b48c8)