数据库与持久化

SQL 客户端模板(SQL Client Templates)

SQL Client Templates 是一个小巧的工具库,专为简化 SQL 查询的构建与执行而设计。

引入依赖

要在项目中使用 SQL Client Templates,请在构建描述文件的 dependencies 配置节中添加以下依赖:

  • Maven(在 pom.xml 中):

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-sql-client-templates</artifactId>
  <version>5.2.1</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

dependencies {
  implementation 'io.vertx:vertx-sql-client-templates:5.2.1'
}

快速入门

以下是使用 SQL 模板的最简单方式。

SQL 模板消费具名参数(Named Parameters),因此(默认情况下)接收一个 Map 作为参数源,而不是元组(Tuple)。

SQL 模板(默认情况下)像客户端的 PreparedQuery 一样产生 RowSet<Row>。事实上,模板本质上是 PreparedQuery 的一层轻量封装。

Map<String, Object> parameters = Collections.singletonMap("id", 1);

SqlTemplate
  .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
  .execute(parameters)
  .onSuccess(users -> {
    users.forEach(row -> {
      System.out.println(row.getString("first_name") + " " + row.getString("last_name"));
    });
  });

当你需要执行插入或更新操作且无需关心返回结果时,可以使用 SqlTemplate.forUpdate:

Map<String, Object> parameters = new HashMap<>();
parameters.put("id", 1);
parameters.put("firstName", "Dale");
parameters.put("lastName", "Cooper");

SqlTemplate
  .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
  .execute(parameters)
  .onSuccess(v -> {
    System.out.println("Successful update");
  });

使用 SqlTemplate.forQuery 或 SqlTemplate.forUpdate 创建模板实例涉及解析 SQL 查询,这会带来一定的解析开销。

为避免反复支付重新解析实际查询的开销,建议复用模板实例。通常,你可以将模板实例保存为 Verticle 的成员变量。

当你的模板必须在事务中执行时,可以使用 withClient 创建临时实例:

SqlTemplate<Map<String, Object>, RowSet<UserDataObject>> template = SqlTemplate
  .forQuery(pool, "SELECT * FROM users WHERE id=#{id}")
  .mapTo(UserDataObjectRowMapper.INSTANCE);

// 保存解析好的模板实例以供复用(通常作为 Verticle 的成员变量)。

// 然后在事务内部创建一个临时实例进行执行

Future<RowSet<UserDataObject>> future = pool.withTransaction(conn -> {
  return template.withClient(conn) // 创建临时实例,用完即弃,切勿长期复用
    .execute(Map.of("id", 1));
});

模板语法

模板采用 #{XXX} 语法,其中 XXX 是合法的 Java 标识符 字符串(不受 Java 关键字保留限制)。

你可以使用反斜杠 \ 转义 字符;例如 \{foo} 将被解释为普通字符串 #{foo},而不会被当作 foo 参数处理。

行映射(Row Mapping)

默认情况下,模板生成 Row 结果类型。

你可以提供自定义的 RowMapper 来实现行级对象映射:

RowMapper<User> ROW_USER_MAPPER = row -> {
  User user = new User();
  user.id = row.getInteger("id");
  user.firstName = row.getString("firstName");
  user.lastName = row.getString("lastName");
  return user;
};

进而实现行级实体映射:

SqlTemplate
  .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
  .mapTo(ROW_USER_MAPPER)
  .execute(Collections.singletonMap("id", 1))
  .onSuccess(users -> {
    users.forEach(user -> {
      System.out.println(user.firstName + " " + user.lastName);
    });
  });

贫血式 JSON 行映射

贫血式 JSON 行映射是一种简单直观的映射方式,通过 toJson 直接将模板行结果的列转换为 JSON 对象:

SqlTemplate
  .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
  .mapTo(Row::toJson)
  .execute(Collections.singletonMap("id", 1))
  .onSuccess(users -> {
    users.forEach(user -> {
      System.out.println(user.encode());
    });
  });

参数映射(Parameters Mapping)

模板默认消费 Map<String, Object> 作为输入。

你可以提供自定义映射器:

TupleMapper<User> PARAMETERS_USER_MAPPER = TupleMapper.mapper(user -> {
  Map<String, Object> parameters = new HashMap<>();
  parameters.put("id", user.id);
  parameters.put("firstName", user.firstName);
  parameters.put("lastName", user.lastName);
  return parameters;
});

从而实现直接将业务对象映射为模板参数:

User user = new User();
user.id = 1;
user.firstName = "Dale";
user.firstName = "Cooper";

SqlTemplate
  .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
  .mapFrom(PARAMETERS_USER_MAPPER)
  .execute(user)
  .onSuccess(res -> {
    System.out.println("User inserted");
  });

还可以非常便捷地执行批量操作:

SqlTemplate
  .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
  .mapFrom(PARAMETERS_USER_MAPPER)
  .executeBatch(users)
  .onSuccess(res -> {
    System.out.println("Users inserted");
  });

贫血式 JSON 参数映射

贫血式 JSON 参数映射在模板参数与 JSON 对象之间建立平凡的映射转换:

JsonObject user = new JsonObject();
user.put("id", 1);
user.put("firstName", "Dale");
user.put("lastName", "Cooper");

SqlTemplate
  .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
  .mapFrom(TupleMapper.jsonObject())
  .execute(user)
  .onSuccess(res -> {
    System.out.println("User inserted");
  });

基于 Jackson Databind 进行映射

你可以利用 Jackson Databind 的强大功能来进行对象映射。

需要在构建描述文件的 dependencies 配置节中添加 Jackson Databind 依赖:

  • Maven(在 pom.xml 中):

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>${jackson.version}</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

dependencies {
  compile 'com.fasterxml.jackson.core:jackson-databind:${jackson.version}'
}

行映射通过将结果行的键/值对构建为 JsonObject,随后调用 mapTo 将其通过 Jackson Databind 反序列化为任意 Java 类:

SqlTemplate
  .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
  .mapTo(User.class)
  .execute(Collections.singletonMap("id", 1))
  .onSuccess(users -> {
    users.forEach(user -> {
      System.out.println(user.firstName + " " + user.lastName);
    });
  });

类似地,参数映射通过调用 JsonObject.mapFrom 将 Java 对象转换为 JsonObject,再提取键/值对来生成模板参数:

User u = new User();
u.id = 1;

SqlTemplate
  .forUpdate(client, "INSERT INTO users VALUES (#{id},#{firstName},#{lastName})")
  .mapFrom(User.class)
  .execute(u)
  .onSuccess(res -> {
    System.out.println("User inserted");
  });

Java Date/Time API 映射

借助 Jackson 的 jackson-modules-java8 扩展模块,你可以映射 java.time 类型。

需要在构建描述文件的 dependencies 配置节中添加 Jackson JSR 310 数据类型依赖:

  • Maven(在 pom.xml 中):

<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</artifactId>
  <version>${jackson.version}</version>
</dependency>
  • Gradle(在 build.gradle 文件中):

dependencies {
  compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jackson.version}'
}

随后需要将时间模块注册到 Jackson 的 ObjectMapper 中:

ObjectMapper mapper = io.vertx.core.json.jackson.DatabindCodec.mapper();

mapper.registerModule(new JavaTimeModule());

现在你可以正常使用诸如 LocalDateTime 等 java.time 类型:

public class LocalDateTimePojo {

  public LocalDateTime localDateTime;

}

基于 Vert.x 数据对象(Data Object)进行映射

SQL Client Templates 组件能够为 Vert.x 数据对象自动生成映射函数。

Vert.x 数据对象是指标注有 @DataObject 注解的标准 Java Bean 类:

@DataObject
class UserDataObject {

  private long id;
  private String firstName;
  private String lastName;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

代码生成(Code Generation)

任何标注有 @RowMapped 或 @ParametersMapped 的数据对象,都会触发生成对应的 Mapper 类。

codegen 注解处理器在编译期自动生成这些类。 这是 Java 编译器的原生特性,因此无需额外步骤,只需在构建工具中正确配置即可。

在构建配置中添加 io.vertx:vertx-codegen:processor 与 io.vertx:vertx-sql-client-templates 依赖。

以下是 Maven 的配置示例:

<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-codegen</artifactId>
  <version>5.2.1</version>
  <classifier>processor</classifier>
</dependency>
<dependency>
  <groupId>io.vertx</groupId>
  <artifactId>vertx-sql-client-templates</artifactId>
  <version>5.2.1</version>
</dependency>

在 Gradle 中同样可以使用该特性:

annotationProcessor "io.vertx:vertx-codegen:5.2.1:processor"
annotationProcessor "io.vertx:vertx-sql-client-templates:5.2.1"
compile "io.vertx:vertx-sql-client-templates:5.2.1"
compile "io.vertx:vertx-codegen-json:5.2.1"

各类常用 IDE 通常自带对注解处理器的支持。

带有 processor classifier 的 codegen JAR 包会通过 META-INF/services 服务发现机制自动启用服务代理注解处理器。

如果你希望配合普通 JAR 包使用,需要显式声明注解处理器,例如在 Maven 中:

<plugin>
  <artifactId>maven-compiler-plugin</artifactId>
  <configuration>
    <annotationProcessors>
      <annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
    </annotationProcessors>
  </configuration>
</plugin>

codegen processor 要求在被注解类所在包的根路径下提供一个标注有 @io.vertx.codegen.annotations.ModuleGen 的 package-info.java 文件。 示例如下:

package-info.java 文件
@ModuleGen(name = "templates", groupPackage = "org.acme")
package org.acme.templates;

import io.vertx.codegen.annotations.ModuleGen;

行映射代码生成

你可以通过在数据对象上标注 @RowMapped 来自动生成行映射器:

@DataObject
@RowMapped
class UserDataObject {

  private long id;
  private String firstName;
  private String lastName;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

默认情况下,每列名称会直接与数据对象的属性名称进行绑定(例如 userName 属性绑定到 userName 列)。

借助 @Column 注解,你可以指定自定义列名:

@DataObject
@RowMapped
class UserDataObject {

  private long id;
  @Column(name = "first_name")
  private String firstName;
  @Column(name = "last_name")
  private String lastName;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

你可以将该注解标注在字段、Getter 或 Setter 方法上。

生成的映射器可按照行映射章节中介绍的方式执行行映射:

SqlTemplate
  .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
  .mapTo(UserDataObjectRowMapper.INSTANCE)
  .execute(Collections.singletonMap("id", 1))
  .onSuccess(users -> {
    users.forEach(user -> {
      System.out.println(user.getFirstName() + " " + user.getLastName());
    });
  });

参数映射代码生成

你可以通过在数据对象上标注 @ParametersMapped 来自动生成参数映射器:

@DataObject
@ParametersMapped
class UserDataObject {

  private long id;
  private String firstName;
  private String lastName;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

默认情况下,每个参数直接与数据对象的属性名称进行绑定(例如 userName 属性绑定到 userName 参数)。

借助 @TemplateParameter 注解,你可以指定自定义参数名:

@DataObject
@ParametersMapped
class UserDataObject {

  private long id;
  @TemplateParameter(name = "first_name")
  private String firstName;
  @TemplateParameter(name = "last_name")
  private String lastName;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }

  public String getFirstName() {
    return firstName;
  }

  public void setFirstName(String firstName) {
    this.firstName = firstName;
  }

  public String getLastName() {
    return lastName;
  }

  public void setLastName(String lastName) {
    this.lastName = lastName;
  }
}

你可以将该注解标注在字段、Getter 或 Setter 方法上。

生成的映射器可按照参数映射章节中介绍的方式执行参数映射:

UserDataObject user = new UserDataObject().setId(1);

SqlTemplate
  .forQuery(client, "SELECT * FROM users WHERE id=#{id}")
  .mapFrom(UserDataObjectParamMapper.INSTANCE)
  .execute(user)
  .onSuccess(users -> {
    users.forEach(row -> {
      System.out.println(row.getString("firstName") + " " + row.getString("lastName"));
    });
  });

Java 枚举类型映射

在底层客户端支持的情况下(例如响应式 PostgreSQL 客户端),你可以直接映射 Java 枚举类型。

通常,Java 枚举类型会被映射为字符串、数值或数据库特定的自定义枚举类型。

命名格式规范

默认模板对参数和列使用相同的大小写命名。你可以在 @Column 和 @TemplateParameter 注解中覆盖默认命名,并使用你偏好的格式。

你还可以在 @RowMapped 和 @ParametersMapped 注解中配置映射器的特定大小写命名风格:

@DataObject
@RowMapped(formatter = SnakeCase.class)
@ParametersMapped(formatter = QualifiedCase.class)
class UserDataObject {
  // ...
}

支持以下命名风格: