spring-jdbc 的實現原理

前言

本篇文章將回答如下幾個問題html

  1. spring-jdbc 的出現是爲了解決什麼問題
  2. spring-jdbc 如何解決的這些問題
  3. 它的這種技術有何缺陷

首先但願你能帶着這些問題來看這篇文章,也但願這篇文章能讓你很好的解答這些問題。固然,這篇文章的終極目標是但願你可以借鑑spring-jdbc 的思想來解決咱們在工做過程當中所面臨的問題。
若是你想了解,如何使用spring-jdbc,請繞道......java

Dao 模式

爲了實現數據和業務的分離,有人提出了Dao模式。Dao模式是數據處理的一種理想模式,(我認爲)它帶來了兩個方面的好處:一、屏蔽數據訪問的差別性;二、業務與數據分離。spring-jdbc 在本質上是一種Dao模式的具體實現。(Dao模式的詳細介紹
接下下咱們用一個簡單的例子(未具體實現)來簡單介紹一下Dao模式(以下圖所示)spring


從上面的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 exceptionsql

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以後進行處理,因爲屏蔽異常會讓客戶端難以排查問題,若是直接拋出去也帶來更嚴重的問題(必須更改接口且不一樣數據庫所拋出的異常不同),以下所示bash

具體異常所表明的含義:
Spring的DAO異常層次 oracle

異常 什麼時候拋出
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 的封裝過程。app

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

3、缺陷

spring-jdbc的封裝方式獲得了普遍承認,但並不表明它是一個友好的的操做數據庫的工具。 從上面的介紹過程當中,咱們能夠感覺到jdbc 的封裝是面向底層的,因此它對於上層的使用方並不那麼友好。jdbc 並未能真正的實現業務和數據的徹底分離,對callback的定義仍然會穿插在業務當中,因此在實際的業務應用中,已經不多直接使用jdbc。所以spring 也對不少其它的ORM框架進行了支持,如ibatis,hibernate,JDO等等,這些更高級對用戶更加友好。接下我會用一系列文章,對這些框架進行介紹

4、總結

咱們再來回顧一下最前面提出的三個問題:

  1. spring-jdbc 是爲了解決數據和業務分離的問題,使客戶端可以更專一於業務層面,而沒必要關注數據庫資源的鏈接釋放及異常處理等邏輯。
  2. spring-jdbc 採用dao模式實現了業務和數據的分離;使用模板模式,實現了邏輯的封裝
  3. spring-jdbc 屬於面向低層的實現,對用戶不太友好。

我的能力有限,有錯誤之處還請指證.....

相關文章
相關標籤/搜索