目录
Java程序员对JDBC应该都不陌生,使用起来也很简单,只要用DriverManager开启一个Connection,就可以创建Statement或PreparedStatement,然后调用SQL语句,获得ResultSet或将数据修改持久化。但是JDBC使用起来也有很多不便,例如:SQL语句编写在程序代码中,不方便维护;参数传入Statement以及从ResultSet取出数据很麻烦,冗余代码多。
1.使用
假设有一个用户表User,结构如下:
字段 | 数据类型 | 说明 |
---|---|---|
id | INTEGER | 序号,自增主键 |
name | VARCHAR | 用户名 |
password | VARCHAR | 密码 |
首先可以创建一个如下的实体类:
public class User {
private int id;
private String name;
private String password;
// 省略getter/setter
}
然后基于Spring JDBC提供的RowMapper接口,创建表结构同实体类的映射:
public class UserMapper implements RowMapper {
@Override
public Object mapRow(ResultSet resultSet, int i) throws SQLException {
return new User(resultSet.getInt("id"),
resultSet.getString("name"),
resultSet.getString("password"));
}
}
然后创建数据操作接口及其实现类,这里只贴实现类代码:
public class UserServiceImpl implements UserService{
private JdbcTemplate jdbcTemplate;
private void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public User queryUserById(String id) {
return jdbcTemplate.queryForObject("select * from User where id = ?", new Object[] {id}, new UserMapper());
}
@Override
public User queryUserByName(String name) {
// 略
}
@Override
public List<User> queryAllUsers() {
return jdbcTemplate.query("select * from User", new UserMapper());
}
@Override
public void insertUser(User user) {
jdbcTemplate.update("insert into User(name, password) values (?,?)", user.getName(), user.getPassword());
}
@Override
public void updateUser(User user) {
// 和insert类似,略
}
@Override
public void deleteUser(User user) {
// 和insert类似,略
}
}
接下来需要将UserService配置为一个Bean:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<!-- 省略一些连接池配置 -->
</bean>
<bean id="userService" class="hnu.yhc.jdbc.UserServiceImpl">
<property name="dataSource" ref="dataSource"/>
</bean>
使用时,只需要从ApplicationContext#getBean方法,获取userService即可。
可见Spring JDBC的核心就是JdbcTemplate。
2.JdbcTemplate的使用
从代码中可以看到,它是通过传入一个DataSource来实现初始化的。DataSource顾名思义,是数据源,定义了数据驱动、URL、帐号密码等信息。
JdbcTemplate构造方法做了两件事:1.赋值,将传入的dataSource引用赋给成员变量;2.主动调用afterPropertiesSet方法(尽管JdbcTemplate本身也是一个InitializingBean)
afterPropertiesSet方法会做两个检查:1.dataSource是否为空,是则抛出异常;2.如果dataSource不为空,则检查是否懒加载,如果不是(默认会懒加载),则初始化SQLExceptionTranslator为SQLErrorCodeSQLExceptionTranslator,用来解析SQL错误信息。
2.1 数据查询
在示例代码中,使用了queryForObject和query两种,其实都是使用的query方法,并且会将传入的RowMapper封装为RowMapperResultSetExtractor,最终调用的query方法签名为:query(final String sql, final ResultSetExtractor<T> rse)。
内部主要做了两件事,一是创建了一个回调,内部会执行SQL语句,获得结果集,然后使用ResultSetExtractor解析出实体对象,二是调用execute方法运行这个回调。execute方法有很多版本,这里以execute(StatementCallback<T> action)为例。
首先从数据源获取一个连接:
Connection con = DataSourceUtils.getConnection(this.obtainDataSource());
实际调用DataSourceUtils#doGetConnection,根据事务处理需求做了不同处理。如果开启了事务同步,那么会用一个ConnectionHolder存储当前连接,同一事务内再次获取连接时,就可以从Holder直接获取引用了。
接下来就是熟悉的con.createStatement()创建SQL语句对象,并且会设置一些属性,比如超时时间、最大行数等。
接下来就是调用回调的doInStatement方法实际执行语句,例如:
public T doInStatement(Statement stmt) throws SQLException {
ResultSet rs = null;
Object var3;
try {
rs = stmt.executeQuery(sql);
var3 = rse.extractData(rs);
} finally {
JdbcUtils.closeResultSet(rs);
}
return var3;
}
executeQuery都很熟悉了,主要来看extractData方法,RowMapperResultSetExtractor的实现如下:
public List<T> extractData(ResultSet rs) throws SQLException {
List<T> results = this.rowsExpected > 0 ? new ArrayList(this.rowsExpected) : new ArrayList();
int var3 = 0;
while(rs.next()) {
results.add(this.rowMapper.mapRow(rs, var3++));
}
return results;
}
其实就是遍历结果集,对每一行都传入RowMapper的mapRow方法,解析为实体对象,然后塞进List。对于queryForObject这样返回单个对象的方法,只要返回列表中第一个对象即可。
如果过程中出现异常,就会用ExceptionTranslator将SQLException翻译为DataAccessException再抛出。
最终会调用DataSourceUtils.releaseConnection方法将连接释放,其实就是将connectionHolder中的连接引用数减一,直到引用数为0,才尝试真正释放连接。
2.2 数据持久化
JdbcTemplate中,数据插入、更新和删除,都是用update方法处理的。示例代码中使用的是update(String sql, @Nullable Object... args)版本。最终调用execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action)方法。
在传递过程中,首先是将sql语句封装为PreparedStatement,将传入的可变长度参数封装为ArgumentPreparedStatementSetter,它的作用就是按顺序把参数传给PreparedStatement,所以就不用一个个setInt、setString...了,如果没有传入参数类型,会根据参数的TypeName做推断。
然后类似于查询,会将语句和结果处理逻辑都封装进PreparedStatementCallback#doInPreparedStatement方法,execute方法的核心逻辑也差不多,最终也是靠PreparedStatement的executeUpdate方法完成数据发送。