前言
本篇文章將回答以下幾個(gè)問題
- spring-jdbc 的出現(xiàn)是為了解決什么問題
- spring-jdbc 如何解決的這些問題
- 它的這種技術(shù)有何缺陷
首先希望你能帶著這些問題來(lái)看這篇文章,也希望這篇文章能讓你很好的解答這些問題悼院。當(dāng)然虑凛,這篇文章的終極目標(biāo)是希望你能夠借鑒spring-jdbc 的思想來(lái)解決我們?cè)诠ぷ鬟^程中所面臨的問題荒叶。
如果你想了解,如何使用spring-jdbc,請(qǐng)繞道......
Dao 模式
為了實(shí)現(xiàn)數(shù)據(jù)和業(yè)務(wù)的分離交胚,有人提出了Dao模式。Dao模式是數(shù)據(jù)處理的一種理想模式,(我認(rèn)為)它帶來(lái)了兩個(gè)方面的好處:1色难、屏蔽數(shù)據(jù)訪問的差異性;2等缀、業(yè)務(wù)與數(shù)據(jù)分離枷莉。spring-jdbc 在本質(zhì)上是一種Dao模式的具體實(shí)現(xiàn)。(Dao模式的詳細(xì)介紹)
接下下我們用一個(gè)簡(jiǎn)單的例子(未具體實(shí)現(xiàn))來(lái)簡(jiǎn)單介紹一下Dao模式(如下圖所示)
從上面的UML圖可以知道:
- 首先定義了一個(gè)User的操作接口UserDao,它定義了了獲取用戶信息尺迂、添加用戶信息笤妙、更改用戶信息的行為;
- 具體行為的由其實(shí)現(xiàn)類來(lái)實(shí)現(xiàn)噪裕,我們這里舉了兩個(gè)例子:Batis 實(shí)現(xiàn)和Jdbc實(shí)現(xiàn)(當(dāng)然也可以緩存實(shí)現(xiàn)或file實(shí)現(xiàn)等)蹲盘,它實(shí)現(xiàn)具體獲取或修改數(shù)據(jù)的行為;UserDaoFactory 生成具體的實(shí)現(xiàn)UserDao實(shí)現(xiàn)類(請(qǐng)參考下面代碼)膳音。
- 所以當(dāng)我們?cè)赟ervice層(UserService)訪問數(shù)據(jù)時(shí)召衔,只 需要使用UserDaoFactory 生成一個(gè)具體的UserDao實(shí)現(xiàn)類就可以了,這樣業(yè)務(wù)層就可以完全操作數(shù)據(jù)操作的具體實(shí)現(xiàn)( 參考下面UserService的具體實(shí)現(xiàn))
public class User {
private int id;
private String name;
private String email;
private String phone;
public User() {
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public String getEmail() {
return email;
}
public String getPhone() {
return phone;
}
public void setId(int id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setEmail(String email) {
this.email = email;
}
public void setPhone(String phone) {
this.phone = phone;
}
}
public interface UserDaoInterface {
public User getUserInfoByName(String name);
public void putUserInfo(User user);
public void updateUserInfo(User user);
}
public class UserDaoJdbcAccessImpl implements UserDaoInterface {
// Jdbc連接數(shù)據(jù)庫(kù)等操作祭陷,未完成具體實(shí)現(xiàn)
private DataSource dataSource;
public User getUserInfoByName(String name) {
dataSource.getC
return new User();
}
public void putUserInfo(User user) {
}
public void updateUserInfo(User user) {
}
}
public class UserDaoBatisAccessImpl implements UserDaoInterface {
// Batis連接數(shù)據(jù)庫(kù)等操作苍凛,未完成具體實(shí)現(xiàn)
public User getUserInfoByName(String name) {
return new User();
}
public void putUserInfo(User user) {
}
public void updateUserInfo(User user) {
}
}
public class UserDaoFacotry {
public static UserDaoInterface getUserDao(int which) {
switch(which) {
case 1:
return new UserDaoJdbcAccessImpl();
case 2:
return new UserDaoBatisAccessImpl();
default:
return null;
}
}
}
public class UserService {
public UserDaoInterface getUserDaoOperation() {
return UserDaoFacotry.getUserDao(1);
}
public void getUserInfo() {
User user = this.getUserDaoOperation().getUserInfoByName("xiaoming");
}
}
但在具體實(shí)現(xiàn)DaoImpl時(shí)遇到了一個(gè)問題,數(shù)據(jù)庫(kù)的連接訪問會(huì)拋出異常颗胡,且屬于checked exception
public User getUserInfoByName(String name) {
try {
Connection connection = dataSource.getConnection();
User user = ....
return user;
} catch (SQLException e) {
} finally {
connection.close();
}
}
這是很尷尬的毫深,因?yàn)榇藭r(shí)我們不知道是要拋給上層業(yè)務(wù)還是catch之后進(jìn)行處理。catch之后進(jìn)行處理毒姨,由于屏蔽異常會(huì)讓客戶端難以排查問題哑蔫,如果直接拋出去也帶來(lái)更嚴(yán)重的問題(必須更改接口且不同數(shù)據(jù)庫(kù)所拋出的異常不一樣),如下所示
public User getUserInfoByName(String name) throw SQLException, NamingException ... {
try {
Connection connection = dataSource.getConnection();
User user = ....
return user;
} finally {
connection.close();
}
}
jdbc 為了解決不同數(shù)據(jù)庫(kù)帶來(lái)的異常差異化闸迷,則對(duì)異常進(jìn)行統(tǒng)一轉(zhuǎn)換嵌纲,并拋出unchecked異常。具體拋出的異承裙粒可以在org.springframework.dao中查看
這是很尷尬的逮走,因?yàn)榇藭r(shí)我們不知道是要拋給上層業(yè)務(wù)還是catch之后進(jìn)行處理。catch之后進(jìn)行處理今阳,由于屏蔽異常會(huì)讓客戶端難以排查問題师溅,如果直接拋出去也帶來(lái)更嚴(yán)重的問題(必須更改接口且不同數(shù)據(jù)庫(kù)所拋出的異常不一樣),如下所示
具體異常所代表的含義:
Spring的DAO異常層次
| 異常 | 何時(shí)拋出 |
| :-------- :|: --------:|
| CleanupFailureDataAccessException |一項(xiàng)操作成功地執(zhí)行盾舌,但在釋放數(shù)據(jù)庫(kù)資源時(shí)發(fā)生異常(例如墓臭,關(guān)閉一個(gè)Connection |
| DataAccessResourceFailureException |數(shù)據(jù)訪問資源徹底失敗,例如不能連接數(shù)據(jù)庫(kù) |
| iMac | 10000 元 |
|DataIntegrityViolationException| Insert或Update數(shù)據(jù)時(shí)違反了完整性妖谴,例如違反了惟一性限制|
|DataRetrievalFailureException |某些數(shù)據(jù)不能被檢測(cè)到窿锉,例如不能通過關(guān)鍵字找到一條記錄|
|DeadlockLoserDataAccessException| 當(dāng)前的操作因?yàn)樗梨i而失敗|
|IncorrectUpdateSemanticsDataAccessException| Update時(shí)發(fā)生某些沒有預(yù)料到的情況,例如更改超過預(yù)期的記錄數(shù)膝舅。當(dāng)這個(gè)異常被拋出時(shí)嗡载,執(zhí)行著的事務(wù)不會(huì)被回滾|
|InvalidDataAccessApiusageException 一個(gè)數(shù)據(jù)訪問的JAVA| API沒有正確使用,例如必須在執(zhí)行前編譯好的查詢編譯失敗了|
|invalidDataAccessResourceUsageException |錯(cuò)誤使用數(shù)據(jù)訪問資源仍稀,例如用錯(cuò)誤的SQL語(yǔ)法訪問關(guān)系型數(shù)據(jù)庫(kù)|
|OptimisticLockingFailureException |樂觀鎖的失敗洼滚。這將由ORM工具或用戶的DAO實(shí)現(xiàn)拋出|
|TypemismatchDataAccessException| Java類型和數(shù)據(jù)類型不匹配,例如試圖把String類型插入到數(shù)據(jù)庫(kù)的數(shù)值型字段中|
|UncategorizedDataAccessException| 有錯(cuò)誤發(fā)生琳轿,但無(wú)法歸類到某一更為具體的異常中|
spring-jdbc
我們可以將spring-jdbc 看作Dao 模式的一個(gè)最佳實(shí)踐判沟,它只是使用了template模式,實(shí)現(xiàn)了最大化的封裝崭篡,以減少用戶使用的復(fù)雜性。spring-jdbc 提供了兩種模式的封裝吧秕,一種是Template,一種是操作對(duì)象的模式琉闪。操作對(duì)象的模式只是提供了面向?qū)ο蟮囊曈X(template 更像面向過程),其底層的實(shí)現(xiàn)仍然是采用Template砸彬。
接下來(lái)我們將會(huì)了解Template 的封裝過程颠毙。
2.1 Template
還是延用上述例子,如果這里我們需要根據(jù)用戶名查詢用戶的完整信息砂碉,將采用下面的方式實(shí)現(xiàn)查詢
public class UserDaoJdbcAccessImpl implements UserDaoInterface {
// Jdbc連接數(shù)據(jù)庫(kù)等操作蛀蜜,未完成具體實(shí)現(xiàn)
private DataSource dataSource;
public User getUserInfoByName(String name) {
String sql = "....." + name;
Connection connection = null;
try {
connection = DataSourceUtils.getConnection(dataSource);
Statement statement = connection.createStatement();
ResultSet resultSet = statement.executeQuery(sql);
List<User> userList = Lists.newArrayList();
while(resultSet.next()) {
User user = new User();
user.setId(resultSet.getInt(1));
user.setName(name);
user.setEmail(resultSet.getString(3));
user.setPhone(resultSet.getString(4));
userList.add(user);
}
connection.close();
connection = null;
statement.close();
return userList;
} catch (Exception e) {
throw new DaoException(e);
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
log.error(".....");
}
}
}
}
當(dāng)我們只需要完成一個(gè)操作的項(xiàng)目時(shí),這種方式還可以接受增蹭,但當(dāng)項(xiàng)目中有大量的DAO需要操作時(shí)滴某,難免過程中會(huì)出現(xiàn)各種問題,如忘記關(guān)閉連接等。
其實(shí)我們可以發(fā)現(xiàn)整個(gè)的數(shù)據(jù)庫(kù)的操作實(shí)現(xiàn)可以分為四個(gè)部分:資源管理(數(shù)據(jù)庫(kù)的連接關(guān)閉等操作)霎奢、sql執(zhí)行(查詢户誓、更新等)、結(jié)果集的處理(將sql查詢結(jié)果轉(zhuǎn)化)幕侠、異常處理帝美。
那是不是可以將公共部分抽象成一個(gè)模板進(jìn)行使用呢?現(xiàn)在我們來(lái)定義一個(gè)Jdbc的一個(gè)模板
public class JdbcTemplate {
public final Object execute(StatementCallback callback) {
Connection connection = null;
Statement statement = null;
try {
connection = getConnetion();
statement = con.createStatement();
Object ret = callback.doWithStatement(callback);
return retValue;
} catch (SQLException e) {
DateAccessException ex = translateSqlException(e);
throw ex;
} finally {
closeStatement(statement);
releaseConnection(connection);
}
}
}
Template 定義了關(guān)注了操作的所有過程晤硕,只需要傳遞一個(gè)callback,就可以幫我們處理各種細(xì)節(jié)化操作悼潭,這些細(xì)節(jié)化操作包括:獲取數(shù)據(jù)庫(kù)連接;執(zhí)行操作舞箍;處理異常舰褪;資源釋放。那我們?cè)谑褂脮r(shí)就可以簡(jiǎn)化為
private JdbcTemplate jdbcTemplate;
// Jdbc連接數(shù)據(jù)庫(kù)等操作创译,未完成具體實(shí)現(xiàn)
private DataSource dataSource;
public User getUserInfoByName(String name) {
StatementCallback statementCallback = new StatementCallback() {
@Override
public Object doInStatement(Statement stmt) throws SQLException, DataAccessException {
return null;
}
}
return jdbcTemplate.execute(statementCallback);
}
實(shí)際上抵知,Template 在封裝時(shí)遠(yuǎn)比這個(gè)復(fù)雜,接下來(lái)我們就看一下spring-jdbc 是如何對(duì)jdbc進(jìn)行封裝的
JdbcTemplate 實(shí)現(xiàn)了JdbcOperations接口和繼承了JdbcAccessor软族。
JdbcOperations 定義了數(shù)據(jù)庫(kù)的操作,excute刷喜、 query、update 等立砸,它是對(duì)行為的一種封裝掖疮。
JdbcAccessor 封裝了對(duì)資源的操作以及異常的處理,可以看一下源碼颗祝,比較短浊闪。
public abstract class JdbcAccessor implements InitializingBean {
/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private DataSource dataSource;
private SQLExceptionTranslator exceptionTranslator;
private boolean lazyInit = true;
/**
* Set the JDBC DataSource to obtain connections from.
*/
public void setDataSource(DataSource dataSource) {
this.dataSource = dataSource;
}
/**
* Return the DataSource used by this template.
*/
public DataSource getDataSource() {
return this.dataSource;
}
/**
* Specify the database product name for the DataSource that this accessor uses.
* This allows to initialize a SQLErrorCodeSQLExceptionTranslator without
* obtaining a Connection from the DataSource to get the metadata.
* @param dbName the database product name that identifies the error codes entry
* @see SQLErrorCodeSQLExceptionTranslator#setDatabaseProductName
* @see java.sql.DatabaseMetaData#getDatabaseProductName()
*/
public void setDatabaseProductName(String dbName) {
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dbName);
}
/**
* Set the exception translator for this instance.
* <p>If no custom translator is provided, a default
* {@link SQLErrorCodeSQLExceptionTranslator} is used
* which examines the SQLException's vendor-specific error code.
* @see org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator
* @see org.springframework.jdbc.support.SQLStateSQLExceptionTranslator
*/
public void setExceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
this.exceptionTranslator = exceptionTranslator;
}
/**
* Return the exception translator for this instance.
* <p>Creates a default {@link SQLErrorCodeSQLExceptionTranslator}
* for the specified DataSource if none set, or a
* {@link SQLStateSQLExceptionTranslator} in case of no DataSource.
* @see #getDataSource()
*/
public synchronized SQLExceptionTranslator getExceptionTranslator() {
if (this.exceptionTranslator == null) {
DataSource dataSource = getDataSource();
if (dataSource != null) {
this.exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(dataSource);
}
else {
this.exceptionTranslator = new SQLStateSQLExceptionTranslator();
}
}
return this.exceptionTranslator;
}
/**
* Set whether to lazily initialize the SQLExceptionTranslator for this accessor,
* on first encounter of a SQLException. Default is "true"; can be switched to
* "false" for initialization on startup.
* <p>Early initialization just applies if {@code afterPropertiesSet()} is called.
* @see #getExceptionTranslator()
* @see #afterPropertiesSet()
*/
public void setLazyInit(boolean lazyInit) {
this.lazyInit = lazyInit;
}
/**
* Return whether to lazily initialize the SQLExceptionTranslator for this accessor.
* @see #getExceptionTranslator()
*/
public boolean isLazyInit() {
return this.lazyInit;
}
/**
* Eagerly initialize the exception translator, if demanded,
* creating a default one for the specified DataSource if none set.
*/
@Override
public void afterPropertiesSet() {
if (getDataSource() == null) {
throw new IllegalArgumentException("Property 'dataSource' is required");
}
if (!isLazyInit()) {
getExceptionTranslator();
}
}
}
源碼有三個(gè)參數(shù):datasource、exceptionTranslator(轉(zhuǎn)換各種數(shù)據(jù)庫(kù)方案商的不同的數(shù)據(jù)庫(kù)異常)螺戳、lazyInit(延時(shí)加載:是否在applicationContext 初始化時(shí)就進(jìn)行實(shí)例化)
在使用的過程中我們可以看到搁宾,只需要提供一個(gè)statementCallback,就可以實(shí)現(xiàn)對(duì)Dao 的各種操作。spring-jdbc 為了滿足各種場(chǎng)景的需要倔幼,為我們提供了四組不同權(quán)限的callback
在使用的過程中我們可以看到盖腿,只需要提供一個(gè)statementCallback,就可以實(shí)現(xiàn)對(duì)Dao 的各種操作。spring-jdbc 為了滿足各種場(chǎng)景的需要损同,為我們提供了四組不同權(quán)限的callback
| callback | 說明 |
| :-------- :|: --------:|
|CallableStatementCallback|面向存儲(chǔ)過程|
|ConnectionCallback|面向連接的call,權(quán)限最大(但一般情況應(yīng)該避免使用翩腐,造成操作不當(dāng))|
|PreparedStatementCallback|包含查詢?cè)儏?shù)的的callback,可以防止sql 注入|
|StatementCallback|縮小了ConnectionCallback的權(quán)限范圍,不允許操作數(shù)據(jù)庫(kù)的連接|
我們?cè)倏匆幌翵dbcTemplate 的封裝
public <T> T execute(ConnectionCallback<T> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Connection con = DataSourceUtils.getConnection(getDataSource());
try {
Connection conToUse = con;
if (this.nativeJdbcExtractor != null) {
// Extract native JDBC Connection, castable to OracleConnection or the like.
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
else {
// Create close-suppressing Connection proxy, also preparing returned Statements.
conToUse = createConnectionProxy(con);
}
return action.doInConnection(conToUse);
}
catch (SQLException ex) {
// Release Connection early, to avoid potential connection pool deadlock
// in the case when the exception translator hasn't been initialized yet.
DataSourceUtils.releaseConnection(con, getDataSource());
con = null;
throw getExceptionTranslator().translate("ConnectionCallback", getSql(action), ex);
}
finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
有兩個(gè)需要注意的地方
Connection con = DataSourceUtils.getConnection(getDataSource());
這里創(chuàng)建連接使用的是DataSourceUtils,而不是datasource.getConnection,這是由于考慮到了事務(wù)處理的因素膏燃。
if (this.nativeJdbcExtractor != null) {
// Extract native JDBC Connection, castable to OracleConnection or the like.
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}
這里并不一定使用的是jdbc的connection,因?yàn)閖dbc是一種統(tǒng)一化封裝茂卦,而忽略了各個(gè)sql供應(yīng)商的差異性。有時(shí)間我們需要使用某一數(shù)據(jù)庫(kù)的某種特性(比如Oracle sql)時(shí)组哩,就可以通過對(duì)nativeJdbcExtractor來(lái)達(dá)到目的等龙。
JdbcTemplate 還有幾個(gè)演生的template,這里都不再詳細(xì)介紹处渣。
Ok,關(guān)于template 的介紹就到此為止(這里更傾向于介紹各種技術(shù)的實(shí)現(xiàn)原理,而非如何使用)而咆。
2.2 對(duì)象模式
對(duì)象模式其實(shí)只是把Template 中的操作封裝成各個(gè)對(duì)象霍比,而其本質(zhì)的實(shí)現(xiàn)方式仍然是Template
三、缺陷
spring-jdbc的封裝方式得到了廣泛認(rèn)可暴备,但并不代表它是一個(gè)友好的的操作數(shù)據(jù)庫(kù)的工具悠瞬。 從上面的介紹過程中,我們可以感受到j(luò)dbc 的封裝是面向底層的涯捻,所以它對(duì)于上層的使用方并不那么友好浅妆。jdbc 并未能真正的實(shí)現(xiàn)業(yè)務(wù)和數(shù)據(jù)的完全分離,對(duì)callback的定義仍然會(huì)穿插在業(yè)務(wù)當(dāng)中障癌,所以在實(shí)際的業(yè)務(wù)應(yīng)用中凌外,已經(jīng)很少直接使用jdbc。因此spring 也對(duì)很多其它的ORM框架進(jìn)行了支持涛浙,如ibatis,hibernate,JDO等等康辑,這些更高級(jí)對(duì)用戶更加友好。接下我會(huì)用一系列文章轿亮,對(duì)這些框架進(jìn)行介紹
四疮薇、總結(jié)
我們?cè)賮?lái)回顧一下最前面提出的三個(gè)問題:
- spring-jdbc 是為了解決數(shù)據(jù)和業(yè)務(wù)分離的問題,使客戶端能夠更專注于業(yè)務(wù)層面我注,而不必關(guān)注數(shù)據(jù)庫(kù)資源的連接釋放及異常處理等邏輯按咒。
- spring-jdbc 采用dao模式實(shí)現(xiàn)了業(yè)務(wù)和數(shù)據(jù)的分離;使用模板模式但骨,實(shí)現(xiàn)了邏輯的封裝
- spring-jdbc 屬于面向低層的實(shí)現(xiàn)励七,對(duì)用戶不太友好。
個(gè)人能力有限奔缠,有錯(cuò)誤之處還請(qǐng)指證.....