Vert-x-通過異步的方式使用JDBC連接SQL

jopen 8年前發布 | 27K 次閱讀 JDBC SQL Java開發

  • 歡迎關注 http://quanke.name/
  • 交流群: 231419585
  • 轉載請注明出處,謝謝
  • </ul>

    在這篇文章中,我們將會看到怎樣在vert.x應用中使用 HSQL ,當然也可以使用任意JDBC,以及使用vertx-jdbc-client提供的異步的API,這篇文章的代碼在 github 上。

    異步?

    vert.x一個很重要的特點就是它的異步性。使用異步的API,不需要等結果返回,當有結果返回時,vert.x會主動通知。為了說明這個,我們來看一個簡單的例子。

    我們假設有個 add 方法。一般來說,會像 int r = add(1, 1) 這樣來使用它。這是一個同步的API,所以你必須等到返回結果。異步的API會是這樣: add(1, 1, r -> { /*do something with the result*/}) 。在這個版本中,你傳入了一個Handler,當結果計算出來時才被調用。這個方法不返回任何東西,實現如下:

    public void add(int a, int b, Handler<Integer> resultHandler) {
        int r = a + b;
        resultHandler.handle(r);
    }

    為了避免混淆概念,異步API并不是多線程。像我們在add例子里看到的,并沒有涉及多線程。

    異步JDBC

    看了一些基本的異步的API,現在了解下 vertx-jdbc-client 。這個組件能夠讓我們通過 JDBC driver 與數據庫交互。這些交互都是異步的,以前這樣:

    String sql = "SELECT * FROM Products";
    ResultSet rs = stmt.executeQuery(sql);

    現在要這樣:

    connection.query("SELECT * FROM Products", result -> {
            // do something with the result
    });

    這個模型更高效,當結果出來后vert.x通知,避免了等待結果。

    增加maven依賴

    在 pom.xml 文件中增加兩個 Maven dependencies

    <dependency>
      <groupId>io.vertx</groupId>
      <artifactId>vertx-jdbc-client</artifactId>
      <version>3.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.hsqldb</groupId>
      <artifactId>hsqldb</artifactId>
      <version>2.3.3</version>
    </dependency>

    第一個依賴提供了 vertx-jdbc-client ,第二個提供了 HSQL JDBC 的驅動。如果你想使用另外一個數據庫,修改這個依賴,同時你還需要修改 JDBC url 和 JDBC driver 名。

    初始化JDBC client

    創建JDBC 客戶端(client):

    在 MyFirstVerticle 類中,聲明一個新變量 JDBCClient jdbc ,并且在 start 方法中添加:

    jdbc = JDBCClient.createShared(vertx, config(), "My-Whisky-Collection");

    創建了一個JDBC client實例,使用verticle的配置文件配置JDBC client。這個配置文件需要提供下面的配置才能讓JDBC client正常工作:

    • url-JDBC url,例如: jdbc:hsqldb:mem:db?shutdown=true
    • _driver class-JDBC的驅動,例如: org.hsqldb.jdbcDriver

    有了client,接下來需要連接數據庫。連接數據庫是通過使用 jdbc.getConnection 來實現的, jdbc.getConnection 需要傳入一個 Handler<AsyncResult<SQLConnection>> 參數。我們深入的了解下這個類型。首先,這是一個 Handler ,因此當結果準備好時它就會被調用。這個結果是 AsyncResult<SQLConnection> 的一個實例。 AsyncResult 是 vert.x 提供的一個結構,使用它能夠知道連接數據庫的操作是成功或失敗了。如果成功了,它就會提供一個結果,這里結果是一個 SQLConnection 的實例。

    當你接收一個 AsyncResult 的實例時,代碼通常是:

    if (ar.failed()) {
      System.err.println("The operation has failed...: "
          + ar.cause().getMessage());
    } else {
      // Use the result:
      result = ar.result();
     }

    需要獲取到 SQLConnection ,然后啟動 rest 的應用。因為變成了異步的,這需要改變啟動應用的方式。因此,如果將啟動序列劃分成多塊:

    startBackend(
     (connection) -> createSomeData(connection,
         (nothing) -> startWebApp(
             (http) -> completeStartup(http, fut)
         ), fut
     ), fut);
    • startBackend - 獲取 SQLConnection 對象,然后調用下一步
    • createSomeData - 初始化數據庫并插入數據。當完成后,調用下一步
    • startWebApp - 啟動web應用
    • completeStartup - 最后完成啟動

    fut 由vert.x傳入,通知已經啟動或者啟動過程中遇到的問題。

    startBackend 方法:

    private void startBackend(Handler<AsyncResult<SQLConnection>> next, Future<Void> fut) {
        jdbc.getConnection(ar -> {
          if (ar.failed()) {
            fut.fail(ar.cause());
          } else {
            next.handle(Future.succeededFuture(ar.result()));
          }
        });
      }

    這個方法獲取了一個SQLConnection對象,檢查操作是否完成。如果成功,會調用下一步。失敗了,就會報告一個錯誤。其他的方法遵循同樣的模式:

    • 檢查上一步操作是否成功
    • 處理業務邏輯
    • 調用下一步

    SQL

    客戶端已經準備好了,現在寫SQL。從 createSomeData 方法開始,這個方法也是啟動順序中的一部分:

    private void createSomeData(AsyncResult<SQLConnection> result,
        Handler<AsyncResult<Void>> next, Future<Void> fut) {
        if (result.failed()) {
          fut.fail(result.cause());
        } else {
          SQLConnection connection = result.result();
          connection.execute(
              "CREATE TABLE IF NOT EXISTS Whisky (id INTEGER IDENTITY, name varchar(100), " +
              "origin varchar(100))",
              ar -> {
                if (ar.failed()) {
                  fut.fail(ar.cause());
                  connection.close();
                  return;
                }
                connection.query("SELECT * FROM Whisky", select -> {
                  if (select.failed()) {
                    fut.fail(ar.cause());
                    connection.close();
                    return;
                  }
                  if (select.result().getNumRows() == 0) {
                    insert(
                        new Whisky("Bowmore 15 Years Laimrig", "Scotland, Islay"),
                        connection,
                        (v) -> insert(new Whisky("Talisker 57° North", "Scotland, Island"),
                            connection,
                            (r) -> {
                              next.handle(Future.<Void>succeededFuture());
                              connection.close();
                            }));                                                    
                  } else {
                    next.handle(Future.<Void>succeededFuture());
                    connection.close();
                  }
                });
              });
        }
      }

    這個方法檢查 SQLConnection 是否可用,然后執行一些SQL語句。首先,如果表不存在就創建表。看看下面代碼:

    connection.execute(
        SQL statement,
        handler called when the statement has been executed
    )

    handler 接收 AsyncResult<Void> ,例如:只有是通知而已,沒有實際返回的結果。

    關閉連接

    操作完成后,別忘了關閉SQL鏈接。這個連接會被放入連接池并且可以被重復利用。

    在這個 handler 的代碼里,檢查了 statement 是否正確的執行了,如果正確,我們接下來檢查表是否含有數據,如果沒有,將會使用 insert 方法插入數據:

    private void insert(Whisky whisky, SQLConnection connection, Handler<AsyncResult<Whisky>> next) {
      String sql = "INSERT INTO Whisky (name, origin) VALUES ?, ?";
      connection.updateWithParams(sql,
          new JsonArray().add(whisky.getName()).add(whisky.getOrigin()),
          (ar) -> {
            if (ar.failed()) {
              next.handle(Future.failedFuture(ar.cause()));
              return;
            }
            UpdateResult result = ar.result();
            // Build a new whisky instance with the generated id.
            Whisky w = new Whisky(result.getKeys().getInteger(0), whisky.getName(), whisky.getOrigin());
            next.handle(Future.succeededFuture(w));
          });
    }

    這個方法使用帶有INSERT(插入)statement(聲明)的 upateWithParams 方法,且傳入了值。這個方法避免了 SQL 注入。一旦statement執行了(當數據庫沒有此條數據就會創建),就創建一個新的 Whisky 對象,自動生成ID。

    帶有數據庫(SQL)的REST

    上面的方法都是啟動順序的一部分。但是,關于調用REST API的方法又是怎么樣的呢?以 getAll 方法為例。這個方法被web應用前端調用,并檢索存儲的所有的產品:

    private void getAll(RoutingContext routingContext) {
        jdbc.getConnection(ar -> {
          SQLConnection connection = ar.result();
          connection.query("SELECT * FROM Whisky", result -> {
            List<Whisky> whiskies = result.result().getRows().stream().map(Whisky::new).collect(Collectors.toList());
            routingContext.response()
                .putHeader("content-type", "application/json; charset=utf-8")
                .end(Json.encodePrettily(whiskies));
            connection.close(); // Close the connection        
          });
        });
      }

    這個方法獲得了一個 SQLConnection 對象,然后發出一個查詢。一旦獲取到查詢結果,它會像之前的方法一樣寫 HTTP response 。 getOne 、 deleteOne 、 updateOne 和 addOne 方法都是一樣的。注意,在response之后,需要要關閉SQL連接。

    看下傳入到query方法的handler提供的結果。獲取了一個包含了查詢結果的ResultSet。每一行都是一個JsonObject,因此,如果你有一個數據對象使用JsonObject作為唯一的參數,那么創建這個對象很簡單。

    測試

    需要小小的更新下測試程序,增加配置 JDBCClient 。在 MyFirstVerticleTest 類中,將 setUp 方法中創建的 DeploymentOption 對象修改成:

    DeploymentOptions options = new DeploymentOptions()
            .setConfig(new JsonObject()
                .put("http.port", port)
                .put("url", "jdbc:hsqldb:mem:test?shutdown=true")
                .put("driver_class", "org.hsqldb.jdbcDriver")
            );

    除了 http.port ,還配置了 JDBC url 和 JDBC 驅動。測試時,使用的是一個內存數據庫。在 src/test/resources/my-it-config.json 文件中也要做同樣的修改。

    {
      "http.port": ${http.port},
      "url": "jdbc:hsqldb:mem:it-test?shutdown=true",
      "driver_class": "org.hsqldb.jdbcDriver"
    }

    src/main/conf/my-application-conf.json 文件也同樣需要修改,這不是為了測試,而是為了運行這個應用:

    {
      "http.port" : 8082,
      "url": "jdbc:hsqldb:file:db/whiskies",
      "driver_class": "org.hsqldb.jdbcDriver"
    }

    這里這個 JDBC url 和上一個文件的有點不一樣,因為需要將數據庫存儲到硬盤中。

    展示時間!

    開始構建程序:

    mvn clean package

    沒有修改API(沒有更改發布的java文件和REST接口),測試應該是可以順利的運行的。

    啟動應用:

    java -jar target/my-first-app-1.0-SNAPSHOT-fat.jar -conf src/main/conf/my-application-conf.json

    訪問 http://localhost:8082/assets/index.html ,然后,你可以看到這個應用使用的是數據庫了。這一次,就算重啟應用,這些數據仍然在,因為存儲產品被持久化到硬盤里了。

    總結

    這篇文章中,知道了怎么在 vert.x 里使用 JDBC 數據庫,并沒有很多復雜的東西。開始可能會被這個異步的開發模型驚訝到,但是,一旦你開始使用了,你就很難再回去了。

    下一次,我們將看到這個應用怎么使用mongoDB來替換HSQL。

    Stay tuned, and happy coding !

來自: http://www.jianshu.com/p/6725ac7c2143

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!