前言
本篇文章將回答以下幾個問題
- spring-jdbc 的出現是為了解決什麼問題
- spring-jdbc 如何解決的這些問題
- 它的這種技術有何缺陷
首先希望你能帶著這些問題來看這篇文章,也希望這篇文章能讓你很好的解答這些問題。當然,這篇文章的終極目標是希望你能夠借鑑spring-jdbc 的思想來解決我們在工作過程中所面臨的問題。
如果你想了解,如何使用spring-jdbc,請繞道……
Dao 模式
為了實現資料和業務的分離,有人提出了Dao模式。Dao模式是資料處理的一種理想模式,(我認為)它帶來了兩個方面的好處:1、遮蔽資料訪問的差異性;2、業務與資料分離。spring-jdbc 在本質上是一種Dao模式的具體實現。(Dao模式的詳細介紹)
接下下我們用一個簡單的例子(未具體實現)來簡單介紹一下Dao模式(如下圖所示)
從上面的UML圖可以知道:
- 首先定義了一個User的操作介面UserDao,它定義了了獲取使用者資訊、新增使用者資訊、更改使用者資訊的行為;
- 具體行為的由其實現類來實現,我們這裡舉了兩個例子:Batis 實現和Jdbc實現(當然也可以快取實現或file實現等),它實現具體獲取或修改資料的行為;UserDaoFactory 生成具體的實現UserDao實現類(請參考下面程式碼)。
- 所以當我們在Service層(UserService)訪問資料時,只 需要使用UserDaoFactory 生成一個具體的UserDao實現類就可以了,這樣業務層就可以完全運算元據操作的具體實現( 參考下面UserService的具體實現)
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連線資料庫等操作,未完成具體實現
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連線資料庫等操作,未完成具體實現
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");
}
}複製程式碼
但在具體實現DaoImpl時遇到了一個問題,資料庫的連線訪問會丟擲異常,且屬於checked exception
public User getUserInfoByName(String name) {
try {
Connection connection = dataSource.getConnection();
User user = ....
return user;
} catch (SQLException e) {
} finally {
connection.close();
}
}複製程式碼
這是很尷尬的,因為此時我們不知道是要拋給上層業務還是catch之後進行處理。catch之後進行處理,由於遮蔽異常會讓客戶端難以排查問題,如果直接丟擲去也帶來更嚴重的問題(必須更改介面且不同資料庫所丟擲的異常不一樣),如下所示
public User getUserInfoByName(String name) throw SQLException, NamingException ... {
try {
Connection connection = dataSource.getConnection();
User user = ....
return user;
} finally {
connection.close();
}
}複製程式碼
jdbc 為了解決不同資料庫帶來的異常差異化,則對異常進行統一轉換,並丟擲unchecked異常。具體丟擲的異常可以在org.springframework.dao中檢視
這是很尷尬的,因為此時我們不知道是要拋給上層業務還是catch之後進行處理。catch之後進行處理,由於遮蔽異常會讓客戶端難以排查問題,如果直接丟擲去也帶來更嚴重的問題(必須更改介面且不同資料庫所丟擲的異常不一樣),如下所示
具體異常所代表的含義:
Spring的DAO異常層次
異常 | 何時丟擲 |
---|---|
CleanupFailureDataAccessException | 一項操作成功地執行,但在釋放資料庫資源時發生異常(例如,關閉一個Connection |
DataAccessResourceFailureException | 資料訪問資源徹底失敗,例如不能連線資料庫 |
iMac | 10000 元 |
DataIntegrityViolationException | Insert或Update資料時違反了完整性,例如違反了惟一性限制 |
DataRetrievalFailureException | 某些資料不能被檢測到,例如不能通過關鍵字找到一條記錄 |
DeadlockLoserDataAccessException | 當前的操作因為死鎖而失敗 |
IncorrectUpdateSemanticsDataAccessException | Update時發生某些沒有預料到的情況,例如更改超過預期的記錄數。當這個異常被丟擲時,執行著的事務不會被回滾 |
InvalidDataAccessApiusageException 一個資料訪問的JAVA | API沒有正確使用,例如必須在執行前編譯好的查詢編譯失敗了 |
invalidDataAccessResourceUsageException | 錯誤使用資料訪問資源,例如用錯誤的SQL語法訪問關係型資料庫 |
OptimisticLockingFailureException | 樂觀鎖的失敗。這將由ORM工具或使用者的DAO實現丟擲 |
TypemismatchDataAccessException | Java型別和資料型別不匹配,例如試圖把String型別插入到資料庫的數值型欄位中 |
UncategorizedDataAccessException | 有錯誤發生,但無法歸類到某一更為具體的異常中 |
spring-jdbc
我們可以將spring-jdbc 看作Dao 模式的一個最佳實踐,它只是使用了template模式,實現了最大化的封裝,以減少使用者使用的複雜性。spring-jdbc 提供了兩種模式的封裝,一種是Template,一種是操作物件的模式。操作物件的模式只是提供了物件導向的視覺(template 更像程式導向),其底層的實現仍然是採用Template。
接下來我們將會了解Template 的封裝過程。
2.1 Template
還是延用上述例子,如果這裡我們需要根據使用者名稱查詢使用者的完整資訊,將採用下面的方式實現查詢
public class UserDaoJdbcAccessImpl implements UserDaoInterface {
// Jdbc連線資料庫等操作,未完成具體實現
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(".....");
}
}
}
}複製程式碼
當我們只需要完成一個操作的專案時,這種方式還可以接受,但當專案中有大量的DAO需要操作時,難免過程中會出現各種問題,如忘記關閉連線等。
其實我們可以發現整個的資料庫的操作實現可以分為四個部分:資源管理(資料庫的連線關閉等操作)、sql執行(查詢、更新等)、結果集的處理(將sql查詢結果轉化)、異常處理。
那是不是可以將公共部分抽象成一個模板進行使用呢?現在我們來定義一個Jdbc的一個模板
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 定義了關注了操作的所有過程,只需要傳遞一個callback,就可以幫我們處理各種細節化操作,這些細節化操作包括:獲取資料庫連線;執行操作;處理異常;資源釋放。那我們在使用時就可以簡化為
private JdbcTemplate jdbcTemplate;
// Jdbc連線資料庫等操作,未完成具體實現
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);
}複製程式碼
實際上,Template 在封裝時遠比這個複雜,接下來我們就看一下spring-jdbc 是如何對jdbc進行封裝的
JdbcTemplate 實現了JdbcOperations介面和繼承了JdbcAccessor。
JdbcOperations 定義了資料庫的操作,excute、 query、update 等,它是對行為的一種封裝。
JdbcAccessor 封裝了對資源的操作以及異常的處理,可以看一下原始碼,比較短。
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();
}
}
}複製程式碼
原始碼有三個引數:datasource、exceptionTranslator(轉換各種資料庫方案商的不同的資料庫異常)、lazyInit(延時載入:是否在applicationContext 初始化時就進行例項化)
在使用的過程中我們可以看到,只需要提供一個statementCallback,就可以實現對Dao 的各種操作。spring-jdbc 為了滿足各種場景的需要,為我們提供了四組不同許可權的callback
在使用的過程中我們可以看到,只需要提供一個statementCallback,就可以實現對Dao 的各種操作。spring-jdbc 為了滿足各種場景的需要,為我們提供了四組不同許可權的callback
callback | 說明 |
---|---|
CallableStatementCallback | 面向儲存過程 |
ConnectionCallback | 面向連線的call,許可權最大(但一般情況應該避免使用,造成操作不當) |
PreparedStatementCallback | 包含查詢詢引數的的callback,可以防止sql 注入 |
StatementCallback | 縮小了ConnectionCallback的許可權範圍,不允許運算元據庫的連線 |
我們再看一下JdbcTemplate 的封裝
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());
}
}複製程式碼
有兩個需要注意的地方
Connection con = DataSourceUtils.getConnection(getDataSource());複製程式碼
這裡建立連線使用的是DataSourceUtils,而不是datasource.getConnection,這是由於考慮到了事務處理的因素。
if (this.nativeJdbcExtractor != null) {
// Extract native JDBC Connection, castable to OracleConnection or the like.
conToUse = this.nativeJdbcExtractor.getNativeConnection(con);
}複製程式碼
這裡並不一定使用的是jdbc的connection,因為jdbc是一種統一化封裝,而忽略了各個sql供應商的差異性。有時間我們需要使用某一資料庫的某種特性(比如Oracle sql)時,就可以通過對nativeJdbcExtractor來達到目的。
JdbcTemplate 還有幾個演生的template,這裡都不再詳細介紹。
Ok,關於template 的介紹就到此為止(這裡更傾向於介紹各種技術的實現原理,而非如何使用)。
2.2 物件模式
物件模式其實只是把Template 中的操作封裝成各個物件,而其本質的實現方式仍然是Template
三、缺陷
spring-jdbc的封裝方式得到了廣泛認可,但並不代表它是一個友好的的運算元據庫的工具。 從上面的介紹過程中,我們可以感受到jdbc 的封裝是面向底層的,所以它對於上層的使用方並不那麼友好。jdbc 並未能真正的實現業務和資料的完全分離,對callback的定義仍然會穿插在業務當中,所以在實際的業務應用中,已經很少直接使用jdbc。因此spring 也對很多其它的ORM框架進行了支援,如ibatis,hibernate,JDO等等,這些更高階對使用者更加友好。接下我會用一系列文章,對這些框架進行介紹
四、總結
我們再來回顧一下最前面提出的三個問題:
- spring-jdbc 是為了解決資料和業務分離的問題,使客戶端能夠更專注於業務層面,而不必關注資料庫資源的連線釋放及異常處理等邏輯。
- spring-jdbc 採用dao模式實現了業務和資料的分離;使用模板模式,實現了邏輯的封裝
- spring-jdbc 屬於面向低層的實現,對使用者不太友好。
個人能力有限,有錯誤之處還請指證…..