目錄
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方法完成數據發送。