Reactive MySQL Client翻译

1,606 阅读18分钟

Reactive MySQL Client

Reactive MySQL Client是一个注重可扩展性和低性能损耗且拥有简约的API的MySQL客户端

特性

  • 事件驱动
  • 轻量
  • 以连接池方式构建
  • 支持预编译查询缓存
  • 支持游标
  • Row streaming(流式row操作)
  • 支持RxJava 1 和 RxJava 2
  • 使用直接内存(译者:即Direct memory)储存对象以避免不必要的内存拷贝
  • 支持jdk8的Date和Time包
  • 支持储存过程
  • 支持TLS/SSL连接
  • 支持MySQL utilities命令
  • 支持MySQL和MariaDB
  • 支持丰富的字符集和字符编码形式
  • 支持Unix domain socket(译者:即同一主机的进程通讯)

使用方式

将 Reactive MySQL Client添加至你的项目的依赖管理中

  • Maven (在你的 pom.xml中添加):
<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-mysql-client</artifactId>
 <version>3.9.3</version>
</dependency>
  • Gradle (在你的 build.gradle 文件中添加):
dependencies {
 compile 'io.vertx:vertx-mysql-client:3.9.3'
}

开始使用

下面是一个简单的例子教你如何连接,查询和断开连接

MySQLConnectOptions connectOptions = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

//创建一个客户端连接池
MySQLPool client = MySQLPool.pool(connectOptions, poolOptions);

// 一个简单的查询
client
  .query("SELECT * FROM users WHERE id='julien'")
  .execute(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> result = ar.result();
    System.out.println("Got " + result.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }

  // 现在关闭
  client.close();
});

连接到MySQL

大多数情况下你会用连接池去连接MySQL

MySQLConnectOptions connectOptions = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

//连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建一个池化客户端
MySQLPool client = MySQLPool.pool(connectOptions, poolOptions);

这个池化的客户端使用连接池而且任何操作都会从连接池中借取连接进行执行并且最后会将这个连接归还到连接池中

如果你正在使用Vert.x框架则你可以通过下面这种方式传递你的Vertx示例

(译者注:如果不传入则在Vert.x web框架环境下会导致异常)

MySQLConnectOptions connectOptions = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);
// 创建池化客户端
MySQLPool client = MySQLPool.pool(vertx, connectOptions, poolOptions);

若你不再需要连接池你必须去关掉它

pool.close();

当你需要在同一个链接中执行多个操作,你需要获取一个客户端连接connection

你可以很容易的从连接池中获取一个连接

MySQLConnectOptions connectOptions = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

// 创建池化客户端
MySQLPool client = MySQLPool.pool(vertx, connectOptions, poolOptions);

// 从池中获取一个连接
client.getConnection(ar1 -> {

  if (ar1.succeeded()) {

    System.out.println("Connected");

    //获取连接
    SqlConnection conn = ar1.result();

    // 所有的操作都会从同一个连接中执行
    conn
      .query("SELECT * FROM users WHERE id='julien'")
      .execute(ar2 -> {
      if (ar2.succeeded()) {
        conn
          .query("SELECT * FROM users WHERE id='emad'")
          .execute(ar3 -> {
            // Release the connection to the pool
            conn.close();
          });
      } else {
        // 将链接释放到池中
        conn.close();
      }
      });
  } else {
    System.out.println("Could not connect: " + ar1.cause().getMessage());
  }
});

一旦这个连接使用完毕,你必须关闭它让他释放回池中,然后它就能被重用了

Unix Domain Socket

有的时候考虑到简洁,安全或者性能因素,可能会需要通过 Unix Domain Socket进行连接

但是JVM并不支持domain sockets,所以首先你要添加原生传输拓展(native transport extensions)到你的项目中

  • Maven (在你 pom.xml中):
<dependency>
 <groupId>io.netty</groupId>
 <artifactId>netty-transport-native-epoll</artifactId>
 <version>${netty.version}</version>
 <classifier>linux-x86_64</classifier>
</dependency>
  • Gradle (在你 build.gradle 文件中):
dependencies {
 compile 'io.netty:netty-transport-native-epoll:${netty.version}:linux-x86_64'
}
NOTE也可以在linux-aarch64中使用对ARM64的原生epoll支持
NOTE对于你团队中的Mac用户,则可以在osx-x86_64使用netty-transport-native-kqueue

然后在 MySQLConnectOptions#setHost中设置domain socket路径

MySQLConnectOptions connectOptions = new MySQLConnectOptions()
  .setHost("/var/run/mysqld/mysqld.sock")
  .setDatabase("the-db");

// 连接池配置
PoolOptions poolOptions = new PoolOptions()
  .setMaxSize(5);

//创建池化客户端
MySQLPool client = MySQLPool.pool(connectOptions, poolOptions);

// 使用vertx实例创建池化客户端
// 保这个vertx实例已经开启原生输送(native transports)
// vertxOptions.setPreferNativeTransport(true);
MySQLPool client2 = MySQLPool.pool(vertx, connectOptions, poolOptions);

关于原生运输的更多信息可以从Vert.x documentation获取

配置

这里有几个选项供你配置客户端

Data Object(数据对象)

通过指定一个MySQLConnectOptions 数据对象配置客户端

MySQLConnectOptions connectOptions = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret");

// Pool Options
PoolOptions poolOptions = new PoolOptions().setMaxSize(5);

// Create the pool from the data object
MySQLPool pool = MySQLPool.pool(vertx, connectOptions, poolOptions);

pool.getConnection(ar -> {
  // Handling your connection
});

字符集合字符编码配置

响应式的MySQL客户端支持配置字符集和字符编码,并把他们映射到相关的java.nio.charset.Charset,举个例子,你可以像这样为一个连接指定字符编码

MySQLConnectOptions connectOptions = new MySQLConnectOptions();

//设置连接字符编码为utf8取代默认的utf8mb4
connectOptions.setCharset("utf8");

响应式的MySQL客户端将utf8mb4作为默认字符编码。字符串,比如说密码,错误信息这些大部分时候以UTF-8编码

characterEncoding选项用于确定将使用哪个Java字符集来编码字符串,比如查询字符串和参数值,该字符集默认为UTF-8,如果它被设置为null,那么客户端将使用默认的Java字符集代替。

(译者:指MySQLConnectOptions#setCharacterEncoding方法)

你也可以为一个链接像这样指定字符集

MySQLConnectOptions connectOptions = new MySQLConnectOptions();

// 设置连接的字符集为utf8_general_ci而不是默认的 utf8mb4_general_ci
//设置的字符集将覆盖字符编码配置
connectOptions.setCharset("gbk");
connectOptions.setCollation("utf8_general_ci");

注意:在数据对象上设置排序规则将覆盖字符集字符编码选项。你可以执行SQLSHOW COLLATIOIN;SHOW CHARCTER SET;获取服务器支持的排序规则和字符集。更多关于MySQL charsets 和collations的信息可以在 MySQL Reference Manual.中找到。

连接属性

你也可以配置连接属性通过setProperties 或者 addProperty方法,注意setProperties会覆盖默认client属性

MySQLConnectOptions connectOptions = new MySQLConnectOptions();

// 添加一个连接属性
connectOptions.addProperty("_java_version", "1.8.0_212");

//覆盖属性
Map<String, String> attributes = new HashMap<>();
attributes.put("_client_name", "myapp");
attributes.put("_client_version", "1.0.0");
connectOptions.setProperties(attributes);

更多关于客户端连接属性的信息可以从MySQL Reference Manual中找到

useAffectedRows

你可以配置useAffectedRows 选项决定当连接到服务器时是否设置 CLIENT_FOUND_ROWS 标识 .

如果指定了CLIENT_FOUND_ROWS标志,那么受影响的行数是找到的行的数字值,而不是受影响的行

(译者:简单来说: useAffectedRows=true时, update返回值为受影响行数; useAffectedRows=false时, update返回值为where条件匹配的行数,即指定后为与之匹配的行数而不是实际影响的行数,在获取查询结果集中会涉及到)

更多信息可以从MySQL Reference Manual中找到

连接URI

除了配置一个MySQLConnectOptions数据对象,我们还提供了一个方式你帮你在连接时配置一个连接URI:

String connectionUri = "mysql://dbuser:secretpassword@database.server.com:3211/mydb";

//创建通过URI创建连接池
MySQLPool pool = MySQLPool.pool(connectionUri);

// Create the connection from the connection URI
MySQLConnection.connect(vertx, connectionUri, res -> {
  // Handling your connection
});

有关连接字符串格式的更多信息,可以从 MySQL Reference Manual.找到

目前客户端在uri中支持以下参数关键字(关键字不是大小写敏感的)

  • host
  • port
  • user
  • password
  • schema
  • socket
  • useAffectedRows

运行查询

如果你不需要使用事务或者只运行单查询,则你可以直接在池中运行,它会使用其中一个连接运行查询并将结果返回给你

这是一个如何运行简单查询的例子

client
  .query("SELECT * FROM users WHERE id='julien'")
  .execute(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> result = ar.result();
    System.out.println("Got " + result.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

预编译查询

你也可以使用相同的方式进行预编译查询

SQL字符串可使用数据库语法?作为参数占位符

client
  .preparedQuery("SELECT * FROM users WHERE id=?")
  .execute(Tuple.of("julien"), ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println("Got " + rows.size() + " rows ");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

查询方法提供一个用于SELECT查询的异步RowSet实例

client
  .preparedQuery("SELECT first_name, last_name FROM users")
  .execute(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    for (Row row : rows) {
      System.out.println("User " + row.getString(0) + " " + row.getString(1));
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

或者是 UPDATE/INSERT查询

client
  .preparedQuery("INSERT INTO users (first_name, last_name) VALUES (?, ?)")
  .execute(Tuple.of("Julien", "Viet"), ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rows = ar.result();
    System.out.println(rows.rowCount());
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

Row通过索引(译者:即下标)访问你的数据
(译者:"select name,password from user" 那么getString(0)就是name的值)

System.out.println("User " + row.getString(0) + " " + row.getString(1));

或者通过名称

System.out.println("User " + row.getString("first_name") + " " + row.getString("last_name"));

客户端不会在这里做任何多余的操作,因此列名来源自表中的名称,不管SQL文本是怎样的。

(译者:这里的表名指的是最终结果那个表,也就说 使用as为列名起别名,get*()方法的实参也得使用别名)

你可以获取到多种类型的数据

String firstName = row.getString("first_name");
Boolean male = row.getBoolean("male");
Integer age = row.getInteger("age");

你也可以使用缓存的预编译语句执行一次性的预编译查询语句

connectOptions.setCachePreparedStatements(true);
client
  .preparedQuery("SELECT * FROM users WHERE id = ?")
  .execute(Tuple.of("julien"), ar -> {
    if (ar.succeeded()) {
      RowSet<Row> rows = ar.result();
      System.out.println("Got " + rows.size() + " rows ");
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

你可以创建PreparedStatement并且你自己管理它的生命周期

sqlConnection
  .prepare("SELECT * FROM users WHERE id = ?", ar -> {
    if (ar.succeeded()) {
      PreparedStatement preparedStatement = ar.result();
      preparedStatement.query()
        .execute(Tuple.of("julien"), ar2 -> {
          if (ar2.succeeded()) {
            RowSet<Row> rows = ar2.result();
            System.out.println("Got " + rows.size() + " rows ");
            preparedStatement.close();
          } else {
            System.out.println("Failure: " + ar2.cause().getMessage());
          }
        });
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

批量执行

你可以批量执行预编译语句

List<Tuple> batch = new ArrayList<>();
batch.add(Tuple.of("julien", "Julien Viet"));
batch.add(Tuple.of("emad", "Emad Alblueshi"));

// 执行预编译语句批处理
client
  .preparedQuery("INSERT INTO USERS (id, name) VALUES (?, ?)")
  .executeBatch(batch, res -> {
  if (res.succeeded()) {
    // Process rows
    RowSet<Row> rows = res.result();
  } else {
    System.out.println("Batch failed " + res.cause());
  }
});

MySQL LAST_INSERT_ID

你可以获取自增值如果你向表中插入一条记录

client
  .query("INSERT INTO test(val) VALUES ('v1')")
  .execute(ar -> {
    if (ar.succeeded()) {
      RowSet<Row> rows = ar.result();
      long lastInsertId = rows.property(MySQLClient.LAST_INSERTED_ID);
      System.out.println("Last inserted id is: " + lastInsertId);
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

你可以从 How to Get the Unique ID for the Last Inserted Row找到更多信息

使用多个连接

当你需要执行连续的查询(不使用事务),你可以创建一个新的连接或者从池中借用一个:

pool.getConnection(ar1 -> {
  if (ar1.succeeded()) {
    SqlConnection connection = ar1.result();

    connection
      .query("SELECT * FROM users WHERE id='julien'")
      .execute(ar2 -> {
      if (ar1.succeeded()) {
        connection
          .query("SELECT * FROM users WHERE id='paulo'")
          .execute(ar3 -> {
          // Do something with rows and return the connection to the pool
          connection.close();
        });
      } else {
        // Return the connection to the pool
        connection.close();
      }
    });
  }
});

也可以创建预编译查询

connection.prepare("SELECT * FROM users WHERE first_name LIKE ?", ar1 -> {
  if (ar1.succeeded()) {
    PreparedStatement prepared = ar1.result();
    prepared.query().execute(Tuple.of("julien"), ar2 -> {
      if (ar2.succeeded()) {
        // All rows
        RowSet<Row> rows = ar2.result();
      }
    });
  }
});

使用事务

在连接中使用事务

您可以使用SQL BEGIN / COMMIT/ ROLLBACK执行事务,如果您这样做,您必须使用SqlConnection并自己管理它。

或者你也可以SqlConnection的API

pool.getConnection(res -> {
  if (res.succeeded()) {

    // 事务必须使用一个连接
    SqlConnection conn = res.result();

    // 开始一个事务
    Transaction tx = conn.begin();

    // 多个语句
    conn
      .query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')")
      .execute(ar1 -> {
      if (ar1.succeeded()) {
        conn
          .query("INSERT INTO Users (first_name,last_name) VALUES ('Emad','Alblueshi')")
          .execute(ar2 -> {
          if (ar2.succeeded()) {
            //提交事务
            tx.commit(ar3 -> {
              if (ar3.succeeded()) {
                System.out.println("Transaction succeeded");
              } else {
                System.out.println("Transaction failed " + ar3.cause().getMessage());
              }
              // Return the connection to the pool
              conn.close();
            });
          } else {
            // Return the connection to the pool
            conn.close();
          }
        });
      } else {
        // Return the connection to the pool
        conn.close();
      }
    });
  }
});

当数据库服务器报告当前事务失败时(例如臭名昭著的当前事务被中止,命令被忽略,直到事务块结束),事务被回滚并调用abortHandler:

tx.abortHandler(v -> {
  System.out.println("Transaction failed => rollbacked");
});

简化的事务API

当你使用连接池的时候,你可以直接在池中开启事务

从池中取出一个连接,然后开启事务,当事务结束后将连接释放回池中

It borrows a connection from the pool, begins the transaction and releases the connection to the pool when the transaction ends.

pool.begin(res -> {
  if (res.succeeded()) {

    // 获取一个事务
    Transaction tx = res.result();

    // 多个语句
    tx.query("INSERT INTO Users (first_name,last_name) VALUES ('Julien','Viet')")
      .execute(ar1 -> {
      if (ar1.succeeded()) {
        tx.query("INSERT INTO Users (first_name,last_name) VALUES ('Emad','Alblueshi')")
          .execute(ar2 -> {
          if (ar2.succeeded()) {
            // 提交事务
            // 连接将自动地返回池中
            tx.commit(ar3 -> {
              if (ar3.succeeded()) {
                System.out.println("Transaction succeeded");
              } else {
                System.out.println("Transaction failed " + ar3.cause().getMessage());
              }
            });
          }
        });
      } else {
      //当事务失败的时候无需关闭连接,而且连接也会归还到池中
        // No need to close connection as transaction will abort and be returned to the pool
      }
    });
  }
});
NOTE用这种方式开启事务的时候,你的代码无需显式的关闭连接,因为它总会被释放到池中

游标和流

默认情况下,预编译查询执行获取所有的行,你可以使用Cursor来控制你想读的行数:

connection.prepare("SELECT * FROM users WHERE age > ?", ar1 -> {
  if (ar1.succeeded()) {
    PreparedStatement pq = ar1.result();

    // 创建一个游标
    Cursor cursor = pq.cursor(Tuple.of(18));

    // 读取50行
    cursor.read(50, ar2 -> {
      if (ar2.succeeded()) {
        RowSet<Row> rows = ar2.result();

        // 检查是否还有
        if (cursor.hasMore()) {
          // Repeat the process...
            //多次重复操作
        } else {
          // No more rows - close the cursor
            //没有更多的行了 关闭游标
          cursor.close();
        }
      }
    });
  }
});

游标应该在提前释放时关闭:

cursor.read(50, ar2 -> {
  if (ar2.succeeded()) {
    // Close the cursor
    cursor.close();
  }
});

游标操作也支持操作起来更方便的流式API,特别是搭配Rx

connection.prepare("SELECT * FROM users WHERE age > ?", ar1 -> {
  if (ar1.succeeded()) {
    PreparedStatement pq = ar1.result();

    // Fetch 50 rows at a time
    RowStream<Row> stream = pq.createStream(50, Tuple.of(18));

    // Use the stream
    stream.exceptionHandler(err -> {
      System.out.println("Error: " + err.getMessage());
    });
    stream.endHandler(v -> {
      System.out.println("End of stream");
    });
    stream.handler(row -> {
      System.out.println("User: " + row.getString("last_name"));
    });
  }
});

这个流批量读取50行当这些行通过handler后,再读取新的50行,以此类推

The stream can be resumed or paused, the loaded rows will remain in memory until they are delivered and the cursor will stop iterating.

流可以被恢复或暂停,加载的行将储存在内存中,直到它们被使用,然后游标将停止迭代。

MySQL 中的类型的映射关系

目前客户端支持下面这些MySQL类型

  • BOOL,BOOLEAN (java.lang.Byte)
  • TINYINT (java.lang.Byte)
  • TINYINT UNSIGNED(java.lang.Short)
  • SMALLINT (java.lang.Short)
  • SMALLINT UNSIGNED(java.lang.Integer)
  • MEDIUMINT (java.lang.Integer)
  • MEDIUMINT UNSIGNED(java.lang.Integer)
  • INT,INTEGER (java.lang.Integer)
  • INTEGER UNSIGNED(java.lang.Long)
  • BIGINT (java.lang.Long)
  • BIGINT UNSIGNED(io.vertx.sqlclient.data.Numeric)
  • FLOAT (java.lang.Float)
  • FLOAT UNSIGNED(java.lang.Float)
  • DOUBLE (java.lang.Double)
  • DOUBLE UNSIGNED(java.lang.Double)
  • BIT (java.lang.Long)
  • NUMERIC (io.vertx.sqlclient.data.Numeric)
  • NUMERIC UNSIGNED(io.vertx.sqlclient.data.Numeric)
  • DATE (java.time.LocalDate)
  • DATETIME (java.time.LocalDateTime)
  • TIME (java.time.Duration)
  • TIMESTAMP (java.time.LocalDateTime)
  • YEAR (java.lang.Short)
  • CHAR (java.lang.String)
  • VARCHAR (java.lang.String)
  • BINARY (io.vertx.core.buffer.Buffer)
  • VARBINARY (io.vertx.core.buffer.Buffer)
  • TINYBLOB (io.vertx.core.buffer.Buffer)
  • TINYTEXT (java.lang.String)
  • BLOB (io.vertx.core.buffer.Buffer)
  • TEXT (java.lang.String)
  • MEDIUMBLOB (io.vertx.core.buffer.Buffer)
  • MEDIUMTEXT (java.lang.String)
  • LONGBLOB (io.vertx.core.buffer.Buffer)
  • LONGTEXT (java.lang.String)
  • ENUM (java.lang.String)
  • SET (java.lang.String)
  • JSON (io.vertx.core.json.JsonObject, io.vertx.core.json.JsonArray, Number, Boolean, String, io.vertx.sqlclient.Tuple#JSON_NULL)

用于解码的Tuple在储存值的时候会使用以上那些类型

注意:在Java中,无符号数值没有特定的表示形式,因此此客户端将把无符号值转换为相关的Java类型。

隐式类型转换

响应式的MySQL客户端支持在执行预编译语句的时候进行隐式的类型转换

假设一个TIME的列存在于你的表中,以下两个例子都可以正常工作

client
  .preparedQuery("SELECT * FROM students WHERE updated_time = ?")
  .execute(Tuple.of(LocalTime.of(19, 10, 25)), ar -> {
      //处理结果集
});
// 通过隐式类型转换它也可以正常工作
client
  .preparedQuery("SELECT * FROM students WHERE updated_time = ?")
  .execute(Tuple.of("19:10:25"), ar -> {
  // 处理结果集
});

用于编码的MySQL数据类型将从参数值推断出来,下面是类型映射

Parameter value typeencoding MySQL type
nullMYSQL_TYPE_NULL
java.lang.ByteMYSQL_TYPE_TINY
java.lang.BooleanMYSQL_TYPE_TINY
java.lang.ShortMYSQL_TYPE_SHORT
java.lang.IntegerMYSQL_TYPE_LONG
java.lang.LongMYSQL_TYPE_LONGLONG
java.lang.DoubleMYSQL_TYPE_DOUBLE
java.lang.FloatMYSQL_TYPE_FLOAT
java.time.LocalDateMYSQL_TYPE_DATE
java.time.DurationMYSQL_TYPE_TIME
java.time.LocalTimeMYSQL_TYPE_TIME
io.vertx.core.buffer.BufferMYSQL_TYPE_BLOB
java.time.LocalDateTimeMYSQL_TYPE_DATETIME
defaultMYSQL_TYPE_STRING

处理BOOLEAN

在MySQL中BOOLEANBOOL的类型以TINYINT的形式存在。其中为0则代表false,非0值代表true。一个BOOLEAN数据在ROW或者Tuple中储存形式为java.lang.Byte类型,你可以调用Row#getValuejava.lang.Byte的形式获取这个值,或者你可以调用Row#getBooleanjava.lang.Boolean的形式获取这个值

client
  .query("SELECT graduated FROM students WHERE id = 0")
  .execute(ar -> {
  if (ar.succeeded()) {
    RowSet<Row> rowSet = ar.result();
    for (Row row : rowSet) {
      int pos = row.getColumnIndex("graduated");
      Byte value = row.get(Byte.class, pos);
      Boolean graduated = row.getBoolean("graduated");
    }
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

如果你想执行带有BOOLEAN类型参数的预编译语句,你可以很简单地将java.lang.Boolean类型的值直接加入参数集合中

client
  .preparedQuery("UPDATE students SET graduated = ? WHERE id = 0")
  .execute(Tuple.of(true), ar -> {
  if (ar.succeeded()) {
    System.out.println("Updated with the boolean value");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

处理JSON数据

MySQL JSON数据类型由以下Java类型表示

  • String
  • Number
  • Boolean
  • io.vertx.core.json.JsonObject
  • io.vertx.core.json.JsonArray
  • io.vertx.sqlclient.Tuple#JSON_NULL 代表json的空值
Tuple tuple = Tuple.of(
  Tuple.JSON_NULL,
  new JsonObject().put("foo", "bar"),
  3);

// Retrieving json
Object value = tuple.getValue(0); // Expect JSON_NULL

//
value = tuple.get(JsonObject.class, 1); // Expect JSON object

//
value = tuple.get(Integer.class, 2); // Expect 3
value = tuple.getInteger(2); // Expect 3

处理BIT数据

BIT数据类型会被映射为java.lang.Long类型,但是Java没有无符号数值的概念,所以如果您想插入或更新一条最大值为BIT(64)的记录,您可以通过一些技巧将参数设置为-1L

处理TIME

MySQL时间数据类型可以用来表示一天的时间,也可以表示从- 883:59:59883:59:59的时间间隔。在响应性MySQL客户机中,TIME数据类型被映射到java.time.Duration。但您也可以通过Row#getLocalTime获取``java.time.LocalTime`类型的值

处理 NUMERIC

Java的Numeric类型用于表示MySQLNumeric类型。

Numeric numeric = row.get(Numeric.class, 0);
if (numeric.isNaN()) {
  // Handle NaN
} else {
  BigDecimal value = numeric.bigDecimalValue();
}

归约查询数据

你可以使用java中的Collectors与查询api搭配使用

Collector<Row, ?, Map<Long, String>> collector = Collectors.toMap(
  row -> row.getLong("id"),
  row -> row.getString("last_name"));

// Run the query with the collector
client.query("SELECT * FROM users").collecting(collector).execute(ar -> {
    if (ar.succeeded()) {
      SqlResult<Map<Long, String>> result = ar.result();

      // Get the map created by the collector
      Map<Long, String> map = result.value();
      System.out.println("Got " + map);
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

收集器处理不能保留Row引用,因为只有一个行用于处理整个集合。

JavaCollectors提供了许多有趣的预定义收集器,例如,您可以轻松地创建直接从行集创建字符串:

Collector<Row, ?, String> collector = Collectors.mapping(
  row -> row.getString("last_name"),
  Collectors.joining(",", "(", ")")
);

// Run the query with the collector
client.query("SELECT * FROM users").collecting(collector).execute(ar -> {
    if (ar.succeeded()) {
      SqlResult<String> result = ar.result();

      // Get the string created by the collector
      String list = result.value();
      System.out.println("Got " + list);
    } else {
      System.out.println("Failure: " + ar.cause().getMessage());
    }
  });

MySQL 的储存过程

你可以在查询中运行储存过程,可以在查询中运行存储过程。结果将按照MySQL protocol 从服务器查询,无需任何额外操作。

client.query("CREATE PROCEDURE multi() BEGIN\n" +
  "  SELECT 1;\n" +
  "  SELECT 1;\n" +
  "  INSERT INTO ins VALUES (1);\n" +
  "  INSERT INTO ins VALUES (2);\n" +
  "END;").execute(ar1 -> {
  if (ar1.succeeded()) {
    // create stored procedure success
    client
      .query("CALL multi();")
      .execute(ar2 -> {
      if (ar2.succeeded()) {
        // handle the result
        RowSet<Row> result1 = ar2.result();
        Row row1 = result1.iterator().next();
        System.out.println("First result: " + row1.getInteger(0));

        RowSet<Row> result2 = result1.next();
        Row row2 = result2.iterator().next();
        System.out.println("Second result: " + row2.getInteger(0));

        RowSet<Row> result3 = result2.next();
        System.out.println("Affected rows: " + result3.rowCount());
      } else {
        System.out.println("Failure: " + ar2.cause().getMessage());
      }
    });
  } else {
    System.out.println("Failure: " + ar1.cause().getMessage());
  }
});

注意:现在仍然不支持将预编译语句绑定到出参

MySQL LOCAL INFILE

本客户端支持处理LOCAL INFILE请求,如果你想将本地文件导入服务器,你可以使用LOAD DATA LOCAL INFILE '<filename>' INTO TABLE <table>;语句

更多信息查看 MySQL Reference Manual.

认证

MySQL 8.0引入了一种名为caching_sha2_password的新身份验证方法,它是进行身份验证的默认方法。为了使用这种新的身份验证方法连接到服务器,您需要使用安全连接(即安全连接)。启用TLS/SSL)或使用RSA密钥对交换加密的密码,以避免密码泄漏。RSA密钥对在通信过程中自动交换,但是服务器RSA公钥可能在通信过程中被黑客攻击,因为它是在一个不安全的连接上传输的。因此,如果你在一个不安全的连接上,想要避免暴露服务器RSA公共密钥的风险,你可以设置服务器RSA公共密钥如下:

MySQLConnectOptions options1 = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
  .setServerRsaPublicKeyPath("tls/files/public_key.pem"); // configure with path of the public key

MySQLConnectOptions options2 = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
  .setServerRsaPublicKeyValue(Buffer.buffer("-----BEGIN PUBLIC KEY-----\n" +
    "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3yvG5s0qrV7jxVlp0sMj\n" +
    "xP0a6BuLKCMjb0o88hDsJ3xz7PpHNKazuEAfPxiRFVAV3edqfSiXoQw+lJf4haEG\n" +
    "HQe12Nfhs+UhcAeTKXRlZP/JNmI+BGoBduQ1rCId9bKYbXn4pvyS/a1ft7SwFkhx\n" +
    "aogCur7iIB0WUWvwkQ0fEj/Mlhw93lLVyx7hcGFq4FOAKFYr3A0xrHP1IdgnD8QZ\n" +
    "0fUbgGLWWLOossKrbUP5HWko1ghLPIbfmU6o890oj1ZWQewj1Rs9Er92/UDj/JXx\n" +
    "7ha1P+ZOgPBlV037KDQMS6cUh9vTablEHsMLhDZanymXzzjBkL+wH/b9cdL16LkQ\n" +
    "5QIDAQAB\n" +
    "-----END PUBLIC KEY-----\n")); // configure with buffer of the public key

关于caching_sha2_password认证的方法参考MySQL Reference Manual.

使用 SSL/TLS

要配置客户端使用SSL连接,您可以像配置Vert.x的NetClient一样,配置MySQLConnectOptions。所有 SSL模式都受到支持,您可以配置sslmode。默认情况下,客户端处于禁用SSL模式。setSsl(true)等同于setSslMode (VERIFY_CA),而setSsl(false)等同于setSsIMode(DISABLED)

MySQLConnectOptions options = new MySQLConnectOptions()
  .setPort(3306)
  .setHost("the-host")
  .setDatabase("the-db")
  .setUser("user")
  .setPassword("secret")
  .setSslMode(SslMode.VERIFY_CA)
  .setPemTrustOptions(new PemTrustOptions().addCertPath("/path/to/cert.pem"));

MySQLConnection.connect(vertx, options, res -> {
  if (res.succeeded()) {
    // Connected with SSL
  } else {
    System.out.println("Could not connect " + res.cause());
  }
});

更多信息参考 Vert.x documentation.

MySQL utility command

有些时候你可能会需要使用MySQL utility command,我们也提供了对它的支持. 更多信息参考 MySQL utility commands.

COM_PING

你可以使用COM_PING命令来确认服务器是否还活跃,当服务器回应PING时handler回调则会被调用,否则其用不会被调用

connection.ping(ar -> {
  System.out.println("The server has responded to the PING");
});

COM_RESET_CONNECTION

你可以使用COM_RESET_CONNECTION命令重置会话状态,它将重置连接状态就像- user variables - temporary tables - prepared statements

connection.resetConnection(ar -> {
  if (ar.succeeded()) {
    System.out.println("Connection has been reset now");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

COM_CHANGE_USER

您可以更改当前连接的用户,这将执行重新身份验证并重置连接状态,如COM_RESET_CONNECTION

MySQLAuthOptions authenticationOptions = new MySQLAuthOptions()
  .setUser("newuser")
  .setPassword("newpassword")
  .setDatabase("newdatabase");
connection.changeUser(authenticationOptions, ar -> {
  if (ar.succeeded()) {
    System.out.println("User of current connection has been changed.");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

COM_INIT_DB

可以使用COM_INIT_DB命令更改连接的默认模式。

connection.specifySchema("newschema", ar -> {
  if (ar.succeeded()) {
    System.out.println("Default schema changed to newschema");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

COM_STATISTICS

您可以使用COM_STATISTICS命令来获得便于阅读的MySQL服务器中内部状态变量

connection.getInternalStatistics(ar -> {
  if (ar.succeeded()) {
    System.out.println("Statistics: " + ar.result());
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

COM_DEBUG

可以使用COM_DEBUG命令将调试信息转储到MySQL服务器的STDOUT(译者:标准输出)。

connection.debug(ar -> {
  if (ar.succeeded()) {
    System.out.println("Debug info dumped to server's STDOUT");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

COM_SET_OPTION

可以使用COM_SET_OPTION命令为当前连接设置选项。目前只能设置CLIENT_MULTI_STATEMENTS.例如,您可以使用此命令禁用CLIENT_MULTI语句。

connection.setOption(MySQLSetOption.MYSQL_OPTION_MULTI_STATEMENTS_OFF, ar -> {
  if (ar.succeeded()) {
    System.out.println("CLIENT_MULTI_STATEMENTS is off now");
  } else {
    System.out.println("Failure: " + ar.cause().getMessage());
  }
});

MySQL 和 MariaDB 版本的支持情况表

MySQLMariaDB
VersionSupportedVersionSupported
5.510.1
5.610.2
5.710.3
8.010.4

注意:

  • 重置连接的utility 命令不能在 MySQL 5.5, 5.6 和MariaDB 10.1上使用
  • 更改用户的utility命令不支持 MariaDB 10.2 和10.3版本

使用中可能遇到的坑&有益的实践

在使用响应性MySQL客户端时,这里有一些有益实践可以帮助您避免常见的缺陷。

预编译语句总数的限制

有时您可能会遇到臭名昭著的错误:,Can’t create more than max_prepared_stmt_count statements (current value: 16382)这是因为服务器已经达到了预编译语句总数的限制。

您可以调整服务器系统变量max_prepared_stmt_count,但它有一个上限值,因此您无法通过这种方式消除错误。

解决这一问题的最佳方法是启用预处理语句缓存,这样就可以重用具有相同SQL字符串的预处理语句,客户端不必为每个请求创建全新的预处理语句。在执行语句后,预编译句将自动关闭。虽然不能完全消除,但是达到上限的可能可以大大减少。

您还可以通过SqlConnection#prepare接口创建PreparedStatement对象,手动管理预编译语句的生命周期,这样您就可以选择何时释放语句句柄,甚至可以使用使用SQL syntax prepared statement.。

揭秘批处理预编译语句

当你想批量插入数据到数据库时,你可以使用PreparedQuery#executeBatch,它提供了一个简单的APl来处理这个问题。请记住,MySQL本身并不支持批处理协议,因此APl只是一个接一个地执行预处理语句的语法糖,这意味着与通过执行一个带一系列值的预处理语句插入多行这种方法相比,需要更多的网络消耗。

难以处理的 DATE & TIME 数据类型

处理MYSQL日期和时间数据类型(尤其是时区数据类型)非常难以处理,因此响应式MySQL不会对这些值进行神奇的转换

  • MySQL DATETIME数据类型不包含时区信息,所以无论当前会话中的时区是什么,您得到的数据都与您设置的数据相同
  • MySQL时间戳数据类型包含时区信息,所以当您设置或获取值时,它总是由服务器转换为在当前会话中设置的时区。