1、概览 {#1概览}
本文将带你了解 Spring 6.1 中新添加的 JdbcClient
接口,它提供了 Fluent 风格的 API,统一了 JdbcTemplate
和 NamedParameterJdbcTemplate
的 Facade,支持链式操作。
有了 JdbcClient
后就可以使用 Fluent 风格的 API 定义查询、设置参数以及执行数据库操作了。
该功能简化了 JDBC 操作,使其更易读、更易懂。然而,对于 JDBC 批处理操作和存储过程的调用,仍然需要使用 JdbcTemplate
和 NamedParameterJdbcTemplate
类。
本文使用 H2 数据库来演示 JdbcClient
的功能。
2、数据库设置 {#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
表,并在其中插入了 2 条记录。
3、创建 JdbcClient
{#3创建-jdbcclient}
Spring Boot 框架会自动发现 application.properties
中的 DB 连接属性,并在应用启动时创建 JdbcClient
Bean。之后,JdbcClient
Bean 可以在任何类中注入、使用。
下面是一个示例,在 StudentDao
类中注入了 JdbcClient
Bean:
@Repository
class StudentDao {
@Autowired
private JdbcClient jdbcClient;
}
本文将会在这个 StudentDao
中定义方法,帮助理解 JdbcClient
接口。
JdbcClient
接口中还有 create(DataSource dataSource)
、create(JdbcOperations jdbcTemplate)
和 create(NamedParameterJdbcOperations jdbcTemplate)
等静态方法可以创建 JdbcClient
实例。
4、使用 JdbcClient 执行数据库查询 {#4使用-jdbcclient-执行数据库查询}
如前所述,JdbcClient
是 JdbcTemplate
和 NamedParameterJdbcTemplate
的统一 Facade。接下来,看看它是如何支持这两种 Template 的。
4.1、支持隐式位置参数 {#41支持隐式位置参数}
使用占位符 ?
来通过位置绑定 SQL 参数。
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()、set()、single() 和 stream()。
测试如下:
@Test
void givenJdbcClient_whenQueryWithPositionalParams_thenSuccess() {
List<Student> students = studentDao.getStudentsOfGradeStateAndGenderWithPositionalParams(1, "New York", "Male");
assertEquals(6, students.size());
}
还可以通过 params
方法使用可变参数来设置 SQL 参数:
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();
}
除了 params(List<?> values)
,这里还通过 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、通过索引设置位置参数 {#42通过索引设置位置参数}
如果需要设置 SQL 语句参数的位置,可以使用 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、支持 Name / Value 对命名参数 {#43支持-name--value-对命名参数}
JdbcClient
还支持使用占位符 :
绑定命名的 SQL 语句参数,这是 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 设置命名参数 {#44通过-map-设置命名参数}
也可以通过 params(Map<String,?> paramMap)
方法,使用 Map 对象来设置命名参数:
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、使用 JdbClient 执行更新操作 {#5使用-jdbclient-执行更新操作}
与查询一样,JdbcClient
也支持创建、更新和删除记录等数据库操作。与前面的章节类似,可以通过各种重载版本的 param()
和 params()
方法绑定参数。因此,这里不再赘述。
不过,在执行 INSERT
、UPDATE
和 DELETE
SQL 语句时,不调用 query()
方法,而是调用 update() 方法。
在 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)
绑定参数。它有一个额外的 sqlType
参数,用于指定参数的数据类型。最后,update()
方法还会返回受影响的行数。
测试:
@Test
void givenJdbcClient_whenInsertWithNamedParamAndSqlType_thenSuccess() {
Student student = getSampleStudent("Johny Dep", 8, 4, "Male", "New York");
assertEquals(1, studentDao.insertWithSetParamWithNamedParamAndSqlType(student));
}
在上述方法中,getSampleStudent()
返回一个 Student
对象。然后将 Student
对象传递给 insertWithSetParamWithNamedParamAndSqlType()
方法,在 student
表中创建一条新记录。
与 JdbcTemplate
一样,JdbcClient
也有 update(KeyHolder generatedKeyHolder)
方法,用于获取插入记录时生成的自增ID。
6、总结 {#6总结}
本文介绍了 Spring 6.1 中引入的新接口 JdbcClient
,它采用了 Fluent 风格的 API 使代码更容易阅读和理解,并且可以替代旧的 JdbcTemplate
和 NamedParameterJdbcTemplate
执行数据库操作。
参考:https://www.baeldung.com/spring-6-jdbcclient-api