1. 概述
在本教程中,我们将介绍JdbcClient接口,它是Spring Framework 6.1的最新成员。它为JdbcTemplate和NamedParameterJdbcTemplate提供了一个具有统一外观的流式接口,这意味着现在它支持链接类型的操作。我们现在可以以流式的API风格定义查询、设置参数并执行数据库操作。
此功能简化了JDBC操作,使它们更具可读性且更易于理解。但是,我们必须使用旧的JdbcTemplate和NamedParameterJdbcTemplate类来进行JDBC批处理操作和存储过程调用。
在本文中,我们将使用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。此后,JdbcClient bean可以在任何类中自动装配。
以下是一个示例,我们在StudentDao类中注入JdbcClient bean:
@Repository
class StudentDao {
@Autowired
private JdbcClient jdbcClient;
}
我们将在本文中使用StudentDao来定义理解JdbcClient接口的方法。
但是,接口中也有一些静态方法,例如create(DataSource dataSource)、create(JdbcOperations jdbcTemplate)和create(NamedParameterJdbcOperations jdbcTemplate),可以创建JdbcClient实例。
4. 使用JdbcClient执行数据库查询
如前所述,JdbcClient是JdbcTemplate和NamedParameterJdbcTemplate的统一门面。因此,我们将看看它如何支持它们。
4.1 隐式支持位置参数
在本节中,我们将讨论对将位置SQL语句参数与占位符?绑定的支持。基本上,我们将了解它如何支持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()方法时,将执行该语句,并像JdbcTemplate中一样借助RowMapper检索结果。
query()方法还支持ResultSetExtractor和RowCallbackHandler参数,我们将在接下来的部分中看到相关示例。
有趣的是,在调用方法list()之前,不会检索到任何结果。还支持其他终端操作,例如option()、set()、single()和stream()。
现在,我们来看看它是如何工作的:
@Test
void givenJdbcClient_whenQueryWithPositionalParams_thenSuccess() {
List<Student> students = studentDao.getStudentsOfGradeStateAndGenderWithPositionalParams(1, "New York", "Male");
assertEquals(6, students.size());
}
让我们看看如何在可变参数的帮助下完成相同的操作:
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()也有一个重载版本,它接收参数列表。让我们看一个例子:
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(Listvalues)之外,我们还看到了option()方法,它返回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 通过索引显式支持位置参数
如果我们需要设置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 支持带有名称-值对的命名参数
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的命名参数
有趣的是,我们还可以在params(Map 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. 使用JdbcClient执行数据库操作
就像查询一样,JdbcClient也支持数据库操作,例如创建、更新和删除记录。与前面的部分类似,我们还可以通过param()和params()方法的各种重载版本来绑定参数。因此,我们不会重复它们。
但是,为了执行SQL语句而不是调用query()方法,我们将调用update()方法。
下面是向学生表中插入记录的示例:
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)方法来检索执行插入语句时创建的自动生成的键。
6. 总结
在本文中,我们了解了Spring Framework 6.1中引入的新接口JdbcClient。我们看到了这一接口如何执行之前由JdbcTemplate和NamedParameterJdbcTemplate执行的所有操作。此外,由于流式的API风格,代码也变得更易于阅读和理解。
与往常一样,本教程的完整源代码可在GitHub上获得。