Spring JDBC

統一的數據訪問異常體系

問題:DAO模式下,數據訪問接口需要拋出異常,如果異常特定于某種實現,那么使用這個接口的代碼將會與特定的實現綁定,如果更換實現,接口需要拋出新的異常。
解決:

  1. 在DAO實現類內部,將數據訪問異常封裝后以unchecked Exception拋出,因為數據庫異常一般也無法恢復。
  2. DAO實現類提取特定的異常的信息,根據信息拋出不同類型的異常。

DataAccessException

JDBC

問題:

  1. 繁瑣的API,完成一個操作有太多雷同的代碼
  2. API使用容易出錯,需要關閉很多資源。
  3. 異常處理能力不足,一個SQLException,沒有子類化,ErrorCode的規范各個數據庫廠商來制定

解決:
JdbcTemplate

  • 封裝所有基于JDBC的數據訪問代碼
  • 對SQLException提供的異常信息進行了統一轉譯,將JDBC數據異常納入Spring異常
  • 還做了對Spring事務的集成

JdbcTemplate

模板方法模式+CallBack
  1. 通過模板方法模式固定流程,包括獲取資源,執行sql,以及處理異常,關閉資源
  2. 對于獲取到的資源,通過傳入CallBack來操作資源,在CallBack中的方法實現需要的邏輯

CallBack公開的資源:

  • ConnectionCallback
  • StatementCallback
  • PreparedStatementCallback
  • CallableStatementCallback
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());
        }
    }

execute即為模板方法,固定了執行流程,包括獲取資源,捕獲異常并轉譯以及關閉資源,暴露出來的資源由Callback進行使用,CallBack內部的邏輯才是訪問數據庫的用戶邏輯。不同類型的CallBack暴露了不同層級的資源。

DataSourceUtils

獲取Connection,綁定到線程

SQLException轉譯

通過SQLExceptionTranslator將SQLException轉譯到DataAccessException

數據訪問
public void execute(final String sql) throws DataAccessException {
        if (logger.isDebugEnabled()) {
            logger.debug("Executing SQL statement [" + sql + "]");
        }
        class ExecuteStatementCallback implements StatementCallback<Object>, SqlProvider {
            @Override
            public Object doInStatement(Statement stmt) throws SQLException {
                stmt.execute(sql);
                return null;
            }
            @Override
            public String getSql() {
                return sql;
            }
        }
        execute(new ExecuteStatementCallback());
    }

public <T> T query(final String sql, final ResultSetExtractor<T> rse) throws DataAccessException {
        Assert.notNull(sql, "SQL must not be null");
        Assert.notNull(rse, "ResultSetExtractor must not be null");
        if (logger.isDebugEnabled()) {
            logger.debug("Executing SQL query [" + sql + "]");
        }
        class QueryStatementCallback implements StatementCallback<T>, SqlProvider {
            @Override
            public T doInStatement(Statement stmt) throws SQLException {
                ResultSet rs = null;
                try {
                    rs = stmt.executeQuery(sql);
                    ResultSet rsToUse = rs;
                    if (nativeJdbcExtractor != null) {
                        rsToUse = nativeJdbcExtractor.getNativeResultSet(rs);
                    }
                    return rse.extractData(rsToUse);
                }
                finally {
                    JdbcUtils.closeResultSet(rs);
                }
            }
            @Override
            public String getSql() {
                return sql;
            }
        }
        return execute(new QueryStatementCallback());
    }

JdbcTemplate中關于數據訪問的函數非常的多,但細分之后,基本所有的函數最終都是通過StatementCallback來實現的,如上面的兩種,函數內部實現了一個StatementCallback類,然后將SQL語句傳入,完成操作,他們的區別在于execute(String)沒有返回結果,而query需要處理ResultSet。

那么從最簡單的開始

  • queryFor*()
    queryFor*函數的參數的第一個都是String,代表SQL語句
    1. queryForMap
      參數可以加Object...作為查詢參數,或者Object[]以及int[]作為查詢參數以及參數類型
      返回一行結果,Map<String, Object>
    2. queryForObject
      參數除了上述的Object[]之外,一般還有一個Class<T>,代表返回的對象類型,或者是RowMapper<T>
      返回一行結果,T
    3. queryForList
      參數除了上述的Object[]作為查詢參數以外,如果還有Class<T>,那么返回對象,否則返回Map
      返回List,List<T>或者List<Map<String, Object>>

對于queryForMap和queryForObject來說,它們也是調用queryForList實現的,但是保證List中只有一個元素。
而對于上面所有的queryFor*函數來說,它們取到的ResultSet都是通過RowMapper處理的,而RowMapper其實也是會被包裝成RowMapperResutlSetExtractor,調用上面的第二個函數。
如果加了查詢參數,那么不用StatementCallback而是用PreparedStatementCallback,本質也是一樣的。

  • query()
    所以也就是上面的queryFor把參數包裝好以后,會調用query函數,有參數的使用PreparedStatementCallback,沒參數的使用StatementCallback,而對于ResultSet的處理,有三種回調接口:
    1. RowMapper
      處理單行結果,結果由ResultSetExtractor組裝成List
    2. RowCallbackHandler
      處理單行結果,結果自己處理
    3. ResutlSetExtractor
      自己處理ResultSet
      區別:《Spring揭秘》p278

從上面的函數也可以看到,查詢的時候這些ResultSetExtractor最終也是在StatementCallback中使用的。

NamedParameterJdbcTemplate

使用變量名而不是?作為查詢參數的占位符

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容