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();
}
参数 grade
、state
和 gender
按调用 param()
方法的顺序隐式绑定。调用 query()
时执行查询,并通过 RowMapper 映射结果(与 JdbcTemplate 相同)。
query()
方法还支持 ResultSetExtractor
和 RowCallbackHandler
参数,后续章节会展示相关示例。
✅ 关键点:直到调用 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 获取。