Vert.x JDBC 客户端

该客户端允许您使用异步 API 从 Vert.x 应用程序与任何符合 JDBC 规范的数据库进行交互。

客户端 API 由接口 SqlClient 表示。

要使用此项目,请将以下依赖项添加到您的构建描述符的依赖项部分

  • Maven(在您的 pom.xml 中)

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-jdbc-client</artifactId>
  <version>5.0.1</version>
</dependency>
  • Gradle(在您的 build.gradle 文件中)

compile 'io.vertx:vertx-jdbc-client:5.0.1'

使用 Sql 客户端 API

Sql 客户端是 Vert.x 响应式 API,用于与 SQL 数据库通信。对于 MySQLPostgreSQLMSSQLIBM DB2 等流行引擎,该 API 已经有多种实现。然而,由于还有许多其他引擎没有异步驱动程序,JDBC 客户端也实现了相同的 API,以允许使用 JDBC 驱动程序,直到最终实现此类异步支持。

创建数据库连接池

一切都从连接池开始。创建连接池很简单。最简单的示例如下:

JDBCConnectOptions connectOptions = new JDBCConnectOptions()
  .setJdbcUrl("jdbc:h2:~/test")
  .setUser("sa")
  .setPassword("");
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(16);
Pool pool = JDBCPool.pool(vertx, connectOptions, poolOptions);

在本例中,我们正在重用 JDBC 客户端 API 来创建连接池。配置是自由格式的,用户需要查找所选底层连接池所需的属性。

对于类型安全的替代方案,提供了第二种工厂方法。第二种工厂方法确保配置正确(因为其属性和类型由编译器验证),但目前仅适用于 Agroal 连接池。

Pool pool = JDBCPool.pool(
  vertx,
  // configure the connection
  new JDBCConnectOptions()
    // H2 connection string
    .setJdbcUrl("jdbc:h2:~/test")
    // username
    .setUser("sa")
    // password
    .setPassword(""),
  // configure the pool
  new PoolOptions()
    .setMaxSize(16)
    .setName("pool-name")
);

使用连接池

一旦您有了连接池,就可以开始使用您的数据库了,连接池以两种模式运行:

  1. 托管连接模式

  2. 手动连接模式

在托管连接模式下工作时,作为用户,您无需担心从连接池中获取和归还连接。查询可以直接针对连接池运行,连接池会确保在查询终止后获取并归还连接。

pool
  .query("SELECT * FROM user")
  .execute()
  .onFailure(e -> {
    // handle the failure
  })
  .onSuccess(rows -> {
    for (Row row : rows) {
      System.out.println(row.getString("FIRST_NAME"));
    }
  });

这也适用于预编译语句。

pool
  .preparedQuery("SELECT * FROM user WHERE emp_id > ?")
  // the emp id to look up
  .execute(Tuple.of(1000))
  .onFailure(e -> {
    // handle the failure
  })
  .onSuccess(rows -> {
    for (Row row : rows) {
      System.out.println(row.getString("FIRST_NAME"));
    }
  });

使用此模式非常方便,因为它允许您专注于业务逻辑,而不是连接管理。有时可能需要保留顺序和因果关系。在这种情况下,我们需要在手动连接模式下执行查询。

pool
  .getConnection()
  .compose(conn -> conn
    .query("SELECT * FROM user")
    .execute()
    // very important! don't forget to return the connection
    .eventually(conn::close))
  .onSuccess(rows -> {
    for (Row row : rows) {
      System.out.println(row.getString("FIRST_NAME"));
    }
  })
  .onFailure(e -> {
    // handle the failure
  });

当然,同样可以说预编译语句也适用于此模式。

pool
  .getConnection()
  .compose(conn -> conn
    .preparedQuery("SELECT * FROM user WHERE emp_id > ?")
    .execute(Tuple.of(1000))
    // very important! don't forget to return the connection
    .eventually(conn::close))
  .onSuccess(rows -> {
    for (Row row : rows) {
      System.out.println(row.getString("FIRST_NAME"));
    }
  })
  .onFailure(e -> {
    // handle the failure
  });

检索生成的键

生成的键是 JDBC 驱动程序的常见功能。连接池允许您使用特殊属性 JDBCPool.GENERATED_KEYS 检索键。例如:

String sql = "INSERT INTO insert_table (FNAME, LNAME) VALUES (?, ?)";

pool
  .preparedQuery(sql)
  .execute(Tuple.of("Paulo", "Lopes"))
  .onSuccess(rows -> {
    // the generated keys are returned as an extra row
    Row lastInsertId = rows.property(JDBCPool.GENERATED_KEYS);
    // just refer to the position as usual:
    long newId = lastInsertId.getLong(0);
  });

使用存储过程和函数

以前使用过 JDBC 的用户都知道,为了调用函数或存储过程,必须使用接口 CallableStatement。对于大多数数据库引擎来说,这种抽象是人为的,因为 PostgreSQLMySQL 等流行引擎并没有真正特殊的命令来区分可调用语句和任何其他常规语句。

现有的 SQL 客户端 API 设计得更接近线缆协议的实际情况,而不是适应 JDBC 规范,因此您将找不到任何处理可调用语句的特定方式。这种设计选择给 JDBC SQL 客户端带来了一些复杂性,因为我们需要将常规调用进行调整,以符合 JDBC 规范要求并与现有客户端兼容。

简单的 IN 参数映射

考虑以下存储过程:

create procedure new_customer(firstname varchar(50), lastname varchar(50))
  modifies sql data
  insert into customers values (default, firstname, lastname, current_timestamp)

为了从 JDBC 客户端调用此过程,您可以编写如下代码:

String sql = "{call new_customer(?, ?)}";

pool
  .preparedQuery(sql)
  // by default the "IN" argument types are extracted from the
  // type of the data in the tuple, as well as the order
  .execute(Tuple.of("Paulo", "Lopes"))
  .onFailure(e -> {
    // handle the failure
  })
  .onSuccess(rows -> {
    // ... success handler
  });

复杂的 IN / OUT 参数映射

前面的示例展示了如何创建简单的查询。但是,它有一些限制。它将假定参数始终是 IN 类型,并且参数类型是参数对应的 Java 类型,这并非总是有效,例如,在处理 null 时。

在这种情况下,客户端有一个辅助类 SqlOutParam,它允许您明确标记所需参数的类型。不仅是数据类型,还包括它是一个 IN 还是一个 OUT

考虑以下存储过程:

create procedure customer_lastname(IN firstname varchar(50), OUT lastname varchar(50))
  modifies sql data
  select lastname into lastname from customers where firstname = firstname

当查找名字时,此过程将返回所有客户的所有姓氏。因此,我们需要同时映射 IN 参数和 OUT 参数。

String sql = "{call customer_lastname(?, ?)}";

pool
  .preparedQuery(sql)
  // by default the "IN" argument types are extracted from the
  // type of the data in the tuple, as well as the order
  //
  // Note that we now also declare the output parameter and it's
  // type. The type can be a "String", "int" or "JDBCType" constant
  .execute(Tuple.of("John", SqlOutParam.OUT(JDBCType.VARCHAR)))
  .onFailure(e -> {
    // handle the failure
  })
  .onSuccess(rows -> {
    // we can verify if there was a output received from the callable statement
    if (rows.property(JDBCPool.OUTPUT)) {
      // and then iterate the results
      for (Row row : rows) {
        System.out.println(row.getString(0));
      }
    }
  });

有时,您需要将 INOUT 参数都映射到同一个变量。同样,所有这些都由 SqlOutParam 辅助类处理。

SqlOutParam param;

// map IN as "int" as well as "OUT" as VARCHAR
param = SqlOutParam.INOUT(123456, JDBCType.VARCHAR);
// or
param = SqlOutParam.INOUT(123456, "VARCHAR");

// and then just add to the tuple as usual:
Tuple.of(param);