1. 概述

本文将介绍 Spring Framework 6.1 新增的 JdbcClient 接口。它为 JdbcTemplate 和 NamedParameterJdbcTemplate 提供了统一的流式 API 门面,支持链式操作。现在我们可以用流式 API 风格定义查询、设置参数并执行数据库操作

这个特性简化了 JDBC 操作,使代码更易读易懂。但需要注意,对于 JDBC 批量操作和存储过程调用,仍需使用传统的 JdbcTemplate 和 NamedParameterJdbcTemplate。

本文将使用 H2 数据库演示 JdbcClient 的能力。

2. 数据库准备

首先创建我们将使用的 student 表:

CREATE TABLE student (
    student_id INT AUTO_INCREMENT PRIMARY KEY,
    student_name VARCHAR(255) NOT NULL,
    age INT,
    grade INT NOT NULL,
    gender VARCHAR(10) NOT NULL,
    state VARCHAR(100) NOT NULL
);
-- Student 1
INSERT INTO student (student_name, age, grade, gender, state) VALUES ('John Smith', 18, 3, 'Male', 'California');

-- Student 2
INSERT INTO student (student_name, age, grade, gender, state) VALUES ('Emily Johnson', 17, 2, 'Female', 'New York');

--More insert statements...

以上 SQL 脚本创建了 student 表并插入示例数据。

3. 创建 JdbcClient

Spring Boot 会自动检测 application.properties 中的数据库连接配置,并在应用启动时创建 JdbcClient Bean。之后可以在任何类中自动注入该 Bean。

以下是在 StudentDao 类中注入 JdbcClient 的示例:

@Repository
class StudentDao {

    @Autowired
    private JdbcClient jdbcClient;
}

本文将使用 StudentDao 类定义方法来演示 JdbcClient 接口的功能。

⚠️ 注意:JdbcClient 接口还提供了静态方法创建实例:

  • create(DataSource dataSource)
  • create(JdbcOperations jdbcTemplate)
  • create(NamedParameterJdbcOperations jdbcTemplate)

4. 使用 JdbcClient 执行查询

如前所述,JdbcClient 是 JdbcTemplate 和 NamedParameterJdbcTemplate 的统一门面。下面分别展示它对两者的支持。

4.1. 隐式支持位置参数

本节讨论使用 ? 占位符绑定位置参数,展示 JdbcTemplate 的功能支持。

StudentDao 类中的方法:

List<Student> getStudentsOfGradeStateAndGenderWithPositionalParams(int grade, String state, String gender) {
    String sql = "select student_id, student_name, age, grade, gender, state from student"
            + " where grade = ? and state = ? and gender = ?";
    return jdbcClient.sql(sql)
      .param(grade)
      .param(state)
      .param(gender)
      .query(new StudentRowMapper()).list();
}

参数 gradestategender 按调用 param() 方法的顺序隐式绑定。调用 query() 时执行查询,并通过 RowMapper 映射结果(与 JdbcTemplate 相同)。

query() 方法还支持 ResultSetExtractorRowCallbackHandler 参数,后续章节会展示相关示例。

关键点:直到调用 list() 方法才会真正获取结果。其他支持的终端操作包括:

  • optional() - 返回 Optional 结果
  • single() - 返回单个结果
  • stream() - 返回结果流
  • set() - 返回结果集

测试示例:

@Test
void givenJdbcClient_whenQueryWithPositionalParams_thenSuccess() {
    List<Student> students = studentDao.getStudentsOfGradeStateAndGenderWithPositionalParams(1, "New York", "Male");
    assertEquals(6, students.size());
}

使用 Varargs 的简化版本:

Student getStudentsOfGradeStateAndGenderWithParamsInVarargs(int grade, String state, String gender) {
    String sql = "select student_id, student_name, age, grade, gender, state from student"
      + " where grade = ? and state = ? and gender = ? limit 1";
    return jdbcClient.sql(sql)
      .params(grade, state, gender)
      .query(new StudentRowMapper()).single();
}

这里用 params() 替代 param(),接受可变参数,并用 single() 获取单条记录。

测试示例:

@Test
void givenJdbcClient_whenQueryWithParamsInVarargs_thenSuccess() {
    Student student = studentDao.getStudentsOfGradeStateAndGenderWithParamsInVarargs(1, "New York", "Male");
    assertNotNull(student);
}

params() 还有接受 List 参数的重载版本:

Optional<Student> getStudentsOfGradeStateAndGenderWithParamsInList(List params) {
    String sql = "select student_id, student_name, age, grade, gender, state from student"
      + " where grade = ? and state = ? and gender = ? limit 1";
    return jdbcClient.sql(sql)
      .params(params)
      .query(new StudentRowMapper()).optional();
}

这里使用 optional() 返回 Optional<Student> 对象。

测试示例:

@Test
void givenJdbcClient_whenQueryWithParamsInList_thenSuccess() {
    List params = List.of(1, "New York", "Male");
    Optional<Student> optional = studentDao.getStudentsOfGradeStateAndGenderWithParamsInList(params);
    if(optional.isPresent()) {
        assertNotNull(optional.get());            
    } else {
        assertThrows(NoSuchElementException.class, () -> optional.get());
    }
}

4.2. 显式指定位置参数索引

如果需要显式设置参数位置,使用 param(int jdbcIndex, Object value) 方法:

List<Student> getStudentsOfGradeStateAndGenderWithParamIndex(int grade, String state, String gender) {
    String sql = "select student_id, student_name, age, grade, gender, state from student"
      + " where grade = ? and state = ? and gender = ?";
    return jdbcClient.sql(sql)
      .param(1, grade)
      .param(2, state)
      .param(3, gender)
      .query(new StudentResultExtractor());
}

这里显式指定了参数位置索引,并使用了 query(ResultSetExtractor rse) 方法。

测试示例:

@Test
void givenJdbcClient_whenQueryWithParamsIndex_thenSuccess() {
    List<Student> students = studentDao.getStudentsOfGradeStateAndGenderWithParamIndex(
      1, "New York", "Male");
    assertEquals(6, students.size());
}

4.3. 使用键值对支持命名参数

JdbcClient 也支持使用 :name 占位符绑定命名参数(NamedParameterJdbcTemplate 的功能)。

param() 方法可以接受键值对参数:

int getCountOfStudentsOfGradeStateAndGenderWithNamedParam(int grade, String state, String gender) {
    String sql = "select student_id, student_name, age, grade, gender, state from student"
      + " where grade = :grade and state = :state and gender = :gender";
    RowCountCallbackHandler countCallbackHandler = new RowCountCallbackHandler();
    jdbcClient.sql(sql)
      .param("grade", grade)
      .param("state", state)
      .param("gender", gender)
      .query(countCallbackHandler);
    return countCallbackHandler.getRowCount();
}

这里使用命名参数,并通过 query(RowCallbackHandler rch) 处理结果。

测试示例:

@Test
void givenJdbcClient_whenQueryWithNamedParam_thenSuccess() {
    Integer count = studentDao.getCountOfStudentsOfGradeStateAndGenderWithNamedParam(1, "New York", "Male");
    assertEquals(6, count);
}

4.4. 使用 Map 支持命名参数

还可以通过 params(Map<String,?> paramMap) 方法传递参数键值对:

List<Student> getStudentsOfGradeStateAndGenderWithParamMap(Map<String, ?> paramMap) {
    String sql = "select student_id, student_name, age, grade, gender, state from student"
      + " where grade = :grade and state = :state and gender = :gender";
    return jdbcClient.sql(sql)
      .params(paramMap)
      .query(new StudentRowMapper()).list();
}

测试示例:

@Test
void givenJdbcClient_whenQueryWithParamMap_thenSuccess() {
    Map<String, ?> paramMap = Map.of(
      "grade", 1,
      "gender", "Male",
      "state", "New York"
    );
    List<Student> students = studentDao.getStudentsOfGradeStateAndGenderWithParamMap(paramMap);
    assertEquals(6, students.size());
}

5. 使用 JdbcClient 执行数据操作

与查询类似,JdbcClient 也支持创建、更新和删除等数据操作。参数绑定方式与前面相同,不再重复。

注意:执行 SQL 语句时,调用 update() 方法而非 query()

student 表插入记录的示例:

Integer insertWithSetParamWithNamedParamAndSqlType(Student student) {
    String sql = "INSERT INTO student (student_name, age, grade, gender, state)"
      + "VALUES (:name, :age, :grade, :gender, :state)";
    Integer noOfrowsAffected = this.jdbcClient.sql(sql)
      .param("name", student.getStudentName(), Types.VARCHAR)
      .param("age", student.getAge(), Types.INTEGER)
      .param("grade", student.getGrade(), Types.INTEGER)
      .param("gender", student.getStudentGender(), Types.VARCHAR)
      .param("state", student.getState(), Types.VARCHAR)
      .update();
    return noOfrowsAffected;
}

这里使用 param(String name, Object value, int sqlType) 绑定参数,额外指定了 SQL 类型。update() 方法返回受影响的行数。

测试示例:

@Test
void givenJdbcClient_whenInsertWithNamedParamAndSqlType_thenSuccess() {
    Student student = getSampleStudent("Johny Dep", 8, 4, "Male", "New York");
    assertEquals(1, studentDao.insertWithSetParamWithNamedParamAndSqlType(student));
}

关键点:与 JdbcTemplate 类似,JdbcClient 提供 update(KeyHolder generatedKeyHolder) 方法获取插入操作生成的自增键。

6. 总结

本文介绍了 Spring Framework 6.1 新增的 JdbcClient 接口。我们看到这个接口可以执行之前需要 JdbcTemplate 和 NamedParameterJdbcTemplate 完成的所有操作。得益于流式 API 风格,代码变得更简洁易读。

本文使用的完整代码可在 GitHub 获取。


原始标题:A Guide to Spring JdbcClient API | Baeldung