前言
数据访问作为 Spring Framework 的特性之一,提供了事务、DAO 支持、JDBC、O/R 映射等能力。针对关系型数据库的访问,Spring 提供了一个 spring-jdbc
模块,JdbcTemplate 是这个模块的核心类,封装了复杂的 JDBC 操作。
日常开发中,如果不想引入第三方 ORM 框架或者业务比较简单,可以将 JdbcTemplate 作为首选。
概述
JdbTemplate 只是一个普通的类,并非是一个完整的 ORM 框架,目的仅仅是消除 JDBC 使用的样板式代码。JdbcTemplate 支持 Spring 事务管理,并且会将 SQLException 异常转换为 DataAccessException。
从命名及实现来看,它的设计有点类似设计模式中的模板方法,不过它是通过回调控制算法中的特定步骤。它将一些 JDBC 操作的通用流程封装到内部,并将一些必须由用户提供的步骤封装为接口,由用户通过方法参数提供。
从下面的表中可以看出 JDBC 操作过程中,JdbcTemplate 封装的部分与用户需要提供的部分。
步骤 |
Spring |
用户 |
DataSource 配置 |
|
√ |
获取 Connection |
√ |
|
定义 SQL 语句 |
|
√ |
创建 Statement |
√ |
|
设置参数 |
|
√ |
执行 SQL |
√ |
|
遍历 ResultSet |
√ |
|
完成每次迭代 |
|
√ |
处理异常 |
√ |
|
处理事务 |
√ |
|
关闭 Statement、ResultSet、Connection |
√ |
|
实例化
使用 JdbcTemplate,首先需要对其实例化,JdbcTemplate 唯一的依赖是 DataSource
,Spring Framework 环境可以将其声明为 bean 。
@Configuration
public class JdbcConfig {
@Bean
public DataSource dataSource(){
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSource.setDriverClass(Driver.class);
dataSource.setUsername("root");
dataSource.setPassword("12345678");
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setDriverClass(Driver.class);
return dataSource;
}
@Bean
public JdbcTemplate jdbcTemplate(){
return new JdbcTemplate(dataSource());
}
}
然后直接注入即可。
@Service
public class UserService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void addUser() {
jdbcTemplate.update("insert into user(username, password) values('hkp','123')");
}
}
Spring Boot 环境下可以直接直接引入相关 starter。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jdbcartifactId>
<version>2.2.7.RELEASEversion>
dependency>
然后在 applicaiton.properties
文件中进行数据源配置即可。
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test
spring.datasource.username=root
spring.datasource.password=12345678
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=org.springframework.jdbc.datasource.SimpleDriverDataSource
Spring Boot 会自动配置 JdbcTemplate 为 bean,应用可以直接注入。
方法分类
JdbcTemplate 类定义如下。
public class JdbcTemplate extends JdbcAccessor implements JdbcOperations {
}
数据库操作的方法由其实现的接口 JdbcOperations
定义,大概可以分为如下几类:
- 通用执行:类似
Statement.execute
方法,这类方法可以进行任意 CRUD 操作,支持普通 SQL 和存储过程,使用重载方法 execute
表示。
- 查询:类似
Statement.executeQuery
方法,用于 select 操作,使用方法 query*
表示,包括 query
、queryForList
、queryForMap
、queryForObject
、queryForRowSet
。
- 更新:类似
Statement.executeUpdate
方法,用于 insert、update、delete 操作,使用重载方法 update
表示。
- 批量更新:类似
Statement.executeBatch
方法,用于批量更新操作,使用重载方法 batchUpdate
表示。
- 存储过程:存储过程方法底层会调用
CallableStatement.execute
,由方法 call
表示。
回调接口
JdbcTemplate 方法较多,用户不必记住每个方法签名,使用时在 IDE 中输入关键字 execute
、query
、update
、call
、batch
,通过代码提示选择合适的方法即可。
其中 SQL 是用户必须提供的,参数设置是可选的,如果是查询操作也可以自定义映射关系,这些自定义的部分由用户通过回调接口提供。下面是一些可能会用到的回调接口。
Connection 回调
Connection
回调对应的接口是 ConnectionCallback
,这个接口用于通用执行,JdbcTemplate
内部获取到 Connection
之后就会回调这个接口,由用户控制 Statement
获取、SQL 执行、结果处理,JdbcTemplate
会自动处理 Connection
的关闭而无需用户操作。
使用示例如下:
Integer count = jdbcTemplate.execute(new ConnectionCallback<Integer>() {
@Override
public Integer doInConnection(Connection con) throws SQLException, DataAccessException {
PreparedStatement statement = con.prepareStatement("select count(1) as c from user");
ResultSet resultSet = statement.executeQuery();
int count = resultSet.getInt("c");
resultSet.close();
statement.close();
return count;
}
});
Statement 回调
Connection
回调还需要手动创建 Statement
,如果想省去创建 Statement
的工作可以使用 StatementCallback
接口,这个接口也是用于通用查询,JdbcTemplate
会自动处理 Statement
的关闭。
示例代码如下:
Integer count = jdbcTemplate.execute(new StatementCallback<Integer>() {
@Override
public Integer doInStatement(Statement stmt) throws SQLException, DataAccessException {
ResultSet resultSet = stmt.executeQuery("select count(1) as c from user");
int count = resultSet.getInt("c");
resultSet.close();
return count;
}
});
ResultSet 抽取
结果抽取用于将 RestultSet
转换为所需的类型,JdbcTemplate
中有三个接口。
1. ResultSetExtractor
首先是 ResultSetExtractor
,很明显,这个接口也是用于查询,JdbcTemplate
获取到 ResultSet
后就会回调这个接口。
Integer count = jdbcTemplate.query(sql, new ResultSetExtractor<Integer>() {
@Override
public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
return rs.getInt("c");
}
});
2. RowCallbackHandler
使用 ResultSetExtractor
还需要对 ResultSet
进行遍历,如果想省去遍历的工作,并且不需要返回值可以使用 RowCallbackHandler
,这个接口可以处理每次迭代,示例代码如下。
String sql = "select count(1) as c from user";
jdbcTemplate.query(sql, new RowCallbackHandler() {
@Override
public void processRow(ResultSet rs) throws SQLException {
int count = rs.getInt("c");
}
});
3. RowMapper
通常情况下,我们查询还是需要将结果映射为 Java 类的,因此更常用的一个回调接口是 RowMapper
,这个接口可以将每行记录转换为一个 Java 对象。示例如下:
String sql = "select username,password from user";
List<User> list = jdbcTemplate.query(sql, new RowMapper<User>() {
@Override
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
User user = new User().setUsername(rs.getString("username"))
.setPassword(rs.getString("password"));
return user;
}
});
PreparedStatement 回调
PreparedStatement
相关的回调接口在 JdbcTemplate
内部比较多,可以大概做如下划分。
1. PreparedStatement 创建
自定义 PreparedStatement
创建逻辑的回调接口是 PreparedStatementCreator
,这个接口可用于 execute
、query
、update
方法中。示例代码如下。
Integer count = jdbcTemplate.query(new PreparedStatementCreator() {
@Override
public PreparedStatement createPreparedStatement(Connection con) throws SQLException {
return con.prepareStatement("select count(1) as c from user");
}
}, new ResultSetExtractor<Integer>() {
@Override
public Integer extractData(ResultSet rs) throws SQLException, DataAccessException {
return rs.getInt("c");
}
});
2. PreparedStatement 参数设置
JdbcTemplate
中有很多重载方法的参数都支持传入 SQL 中使用的参数,例如下面的方法。
public interface JdbcOperations {
<T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException;
int update(String sql, @Nullable Object... args) throws DataAccessException;
}
如果想手动设置参数可以使用 PreparedStatementSetter
回调方法,JdbcTemplate
创建 PreparedStatement
之后就会回调这个接口。示例代码如下。
String sql = "update user set password = '321' where id = ?";
int count = jdbcTemplate.update(sql, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setInt(1, 1);
}
});
3. PreparedStatement 回调
StatementCallback
回调获取到的是一个 Statement
对象,如果想使用 PreparedStatement
对象,可以使用 PreparedStatementCallback
回调接口,这个接口用于 execute
方法。示例代码如下。
int count = jdbcTemplate.execute("update user set password = '321' where id = 1", new PreparedStatementCallback<Integer>() {
@Override
public Integer doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException {
return ps.executeUpdate();
}
});
4. 批量更新回调
批量更新时有两种设置参数的方式,一种是通过 JdbcTemplate.batchUpdate
方法参数直接设置 SQL 中的参数值,另一种是通过回调的方式,具体又有两种。
BatchPreparedStatementSetter
用于批量更新时手动设置参数。
List<User> list = Arrays.asList(new User().setUsername("zhangsan").setPassword("123"),
new User().setUsername("lisi").setPassword("456"));
String sql = "update user set password = ? where username = ?";=
int[] count = jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setString(1, list.get(i).getPassword());
ps.setString(2, list.get(i).getUsername());
}
@Override
public int getBatchSize() {
return list.size();
}
});
如果批量更新的数据量比较大, 可以将其进行拆分,例如 100 条数据,每 10 条做一次批量更新操作,这时可以使用 ParameterizedPreparedStatementSetter
接口设置参数。
List<User> list = Arrays.asList(new User().setUsername("zhangsan").setPassword("123"),
new User().setUsername("lisi").setPassword("456"));
String sql = "update user set password = ? where username = ?";
int[][] counts = jdbcTemplate.batchUpdate(sql, list, 1, new ParameterizedPreparedStatementSetter<User>() {
@Override
public void setValues(PreparedStatement ps, User argument) throws SQLException {
ps.setString(1, argument.getPassword());
ps.setString(2, argument.getUsername());
}
});
CallableStatement 回调
与 PreparedStatement
回调类似,CallableStatement
也有两个接口分别用户创建 CallableStatement
和设置 CallableStatement
参数,这两个回调接口是 CallableStatementCreator
和 CallableStatementCallback
。使用示例如下。
Integer count = jdbcTemplate.execute(new CallableStatementCreator() {
@Override
public CallableStatement createCallableStatement(Connection con) throws SQLException {
return con.prepareCall("customFun()");
}
}, new CallableStatementCallback<Integer>() {
@Override
public Integer doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException {
return cs.getInt("c");
}
});
常用操作
上面介绍了一些回调接口,这些回调接口在大多数场景下使用并不多,只有在极端场景下才会使用,JdbcTemplate 将这些回调接口进一步封装,例如需要创建 Statement
可以直接在方法参数中指定 SQL、需要设置 SQL 参数值也可以直接通过方法参数传入,只有映射关系可能需要通过 RowMapper
手动配置。
下面总结一些在某些场景下可能会用到的方法。
查询
1. 查询单行单列数据
例如查询符合某些条件的记录数量,可以使用如下方法。
<T> T queryForObject(String sql, Class<T> requiredType, @Nullable Object... args) throws DataAccessException;
2. 查询单行多列数据
查询某条记录,并转换为 Map ,可以使用如下方法。
Map<String, Object> queryForMap(String sql, @Nullable Object... args) throws DataAccessException;
查询某条记录,并转换为所需类型,可以使用如下方法。
<T> T queryForObject(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException;
3. 查询多行单列数据
<T> List<T> queryForList(String sql, @Nullable Object[] args, Class<T> elementType) throws DataAccessException;
4. 查询多行多列数据
查询记录,并转换为 Map 可以使用如下方法。
List<Map<String, Object>> queryForList(String sql, @Nullable Object... args) throws DataAccessException;
查询记录,并转换为自定义类型,可以使用如下方法。
<T> List<T> query(String sql, RowMapper<T> rowMapper, @Nullable Object... args) throws DataAccessException;
5. 小技巧
由于查询最为复杂,如果不确定用哪个方法,可以先查找 query*
开头的方法,然后根据方法返回值类型选择。
更新
这里的更新包含 insert、update、delete 操作。常用方法如下。
int update(String sql, @Nullable Object... args) throws DataAccessException;
批量更新
单条 SQL,不同参数批量更新,,可以使用如下方法。
int[] batchUpdate(String sql, List<Object[]> batchArgs) throws DataAccessException;
如果数据量过大,可以拆分成多次批量更新,使用如下方法。
<T> int[][] batchUpdate(String sql, Collection<T> batchArgs, int batchSize,
ParameterizedPreparedStatementSetter<T> pss) throws DataAccessException;
支持命名参数的 JdbcTemplate
JdbcTemplate
中使用的 SQL 参数使用 ?
表示,设置参数时需要注意按照参数的顺序提供值,如果参数比较多不太方便。
spring-jdbc
模块还提供了一个 NamedParameterJdbcTemplate
类,支持为参数命名。可以使用 :paramName
、:{paramName}
或者 ¶mName
的形式为 SQL 参数指定名称。例如:
select * from user where username = :username
NamedParameterJdbcTemplate
底层使用 JdbcTemplate
,使用前面我们提到的 PreparedStatementCreator
回调接口解析 SQL 并设置参数。
使用 NamedParameterJdbcTemplate
时不能将命名参数和 ?
混合使用,可以使用 Map
提供参数值。类定义如下。
public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations {
private final JdbcOperations classicJdbcTemplate;
public NamedParameterJdbcTemplate(DataSource dataSource) {
Assert.notNull(dataSource, "DataSource must not be null");
this.classicJdbcTemplate = new JdbcTemplate(dataSource);
}
}
很明显,它的设计与 JdbcTemplate
类似,由接口 NamedParameterJdbcOperations
提供 JDBC 操作的方法。部分常用方法如下。
<T> List<T> query(String sql, Map<String, ?> paramMap, RowMapper<T> rowMapper) throws DataAccessException;
int update(String sql, Map<String, ?> paramMap) throws DataAccessException;