前言
数据访问作为 Spring Framework 的特性之一,提供了事务、DAO 支持、JDBC、O/R 映射等能力。针对关系型数据库的访问,Spring 提供了一个 spring-jdbc 模块,JdbcTemplate 是这个模块的核心类,封装了复杂的 JDBC 操作。
日常开发中,如果不想引入第三方 ORM 框架或者业务比较简单,可以将 JdbcTemplate 作为首选。
概述
JdbTemplate 只是一个普通的类,并非是一个完整的 ORM 框架,目的仅仅是消除 JDBC 使用的样板式代码。JdbcTemplate 支持 Spring 事务管理,并且会将 SQLException 异常转换为 DataAccessException。
从命名及实现来看,它的设计有点类似设计模式中的模板方法,不过它是通过回调控制算法中的特定步骤。它将一些 JDBC 操作的通用流程封装到内部,并将一些必须由用户提供的步骤封装为接口,由用户通过方法参数提供。
从下面的表中可以看出 JDBC 操作过程中,JdbcTemplate 封装的部分与用户需要提供的部分。
实例化
使用 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.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.2.7.RELEASE</version> </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;
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;