JDBC介紹和Mybatis運行原理及事務處理


本博客內容非自創,轉載自以下三位,侵刪:

https://juejin.im/post/5ab7bd11f265da23906bfbc5

https://my.oschina.net/fifadxj/blog/785621

https://www.jianshu.com/p/b864aecc0de1

JDBC相關概念

Java程序都是通過JDBC連接數據庫的,通過SQL對數據庫編程,JDBC是由SUN公司提出的一些列規范,只定義了接口規范,具體實現由各個數據庫廠商去實現,它是一種典型的橋接模式。ps:橋接模式是一種結構型設計模式,它的主要特點是把抽象與行為實現分離開來,分別定義接口,可以保持各部分的獨立性以及應對他們的功能擴展。

JDBC規范 

所謂規范,就是自己定義了標准接口,做了如下抽象:用Connection代表和數據庫的連接,用Statement執行SQL,用ResultSet表示SQL返回的結果,提供了對數據的便利。從Connection可以創建Statement,Statement執行查詢得到ResultSet。

上面說的Connection、Statement、ResultSet都應該是接口,具體實現由各個數據庫提供商提供。有了規范,可以通過統一的接口,訪問多種類型的數據庫,可隨便切換數據庫。

數據庫驅動

上面提到,接口的實現由各個廠商提供,那么實現類的類名就會不統一,去創建Connection對象時,代碼就會寫死某個實現類,切換數據庫時,就需要修改代碼,這樣不太好。為了解決這個問題,抽象了Driver驅動的概念。

Connection con=MySqlConnectionImpl("127.0.0.1",3306,"mi_user",userName,pwd);

每個數據庫都需要實現Driver接口,通過Driver可獲得數據庫連接Connection,通過反射機制動態創建。

Class.forName("com.mysql.jdbc.Drier");

同一個程序可能訪問不同的數據庫,通過DriverManager來管理驅動,Driver在初始化的時候,需要注冊到DriverManager中。

DriverManager提供了一個getConnection方法,用於建立數據庫Connection:

Connection con=DriverManager.getConnection("127.0.0.1",3306,"mi_user",userName,pwd);

如果有多個數據庫驅動,DriverManager如何區分呢,需要在數據庫連接url中指定,比如mysql需要添加jdbc:mysql前綴:

String url= "jdbc:mysql://127.0.0.1:3306/mi_user";
Connection con=DriverManager.getConnection(url,userName,pwd)

數據源

數據源DataSource包含連接池和連接池管理2個部分,習慣上稱為連接池。在系統初始化的時候,將數據庫連接作為對象存儲在內存中,當需要訪問數據庫時,從連接池中取出一個已建立的空閑連接對象。

使用數據源,獲取其DataSource對象,通過該對象動態的獲取數據庫連接。另外,DataSource對象可以注冊到名字服務(JNDI)中,可以通過名字服務獲得DataSource對象,無需硬性編碼驅動。

DriverManager是JDBC1提供的,DataSource是JDBC2新增的功能,提供了更好的連接數據源的方法。

Mybatis核心組件:

  • SqlSessionFactoryBuilder:會根據配置信息或代碼來生成SqlSessionFactory;
  • SqlSessionFactory:依靠工廠來生成SqlSession;
  • SqlSession:是一個既可以發送SQL去執行並返回結果,也可以獲取Mapper的接口;
  • SQL Mapper:是MyBatis新設計的組件,由一個Java接口和XML文件構成,需要給出對應的SQL和映射規則。它負責發送SQL去執行,並返回結果。

Mybatis核心流程:

通過SqlSessionFactory獲取SqlSession

獲取MapperProxy

獲取Excutor

 

Mybatis事務:

mybatis-spring的實現很大程度上依賴spring jdbc的事務管理,所以我們先看一下在spring中直接使用jdbc訪問數據庫時是如何處理事務的。無論你是使用@Transactional注解這樣的AOP配置方式,還是TransactionTemplate這樣的編碼方式,最終執行的操作事務的代碼都會是類似下面這樣:
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
PlatformTransactionManager txManager = new DataSourceTransactionManager(dataSource);

TransactionStatus status = txManager.getTransaction(def);
try {
    //get jdbc connection...
    //execute sql...
}
catch (Exception e) {
    txManager.rollback(status);
    throw e;
}
txManager.commit(status);

可以看到PlatformTransactionManager的getTransaction(), rollback(), commit()是spring處理事務的核心api,分別對應事務的開始,提交和回滾。

spring事務處理的一個關鍵是保證在整個事務的生命周期里所有執行sql的jdbc connection和處理事務的jdbc connection始終是同一個。然后執行sql的業務代碼一般都分散在程序的不同地方,如何讓它們共享一個jdbc connection呢?這里spring做了一個前提假設:即一個事務的操作一定是在一個thread中執行,且一個thread中如果有多個不同jdbc connection生成的事務的話,他們必須順序執行,不能同時存在。(這個假設在絕大多數情況下都是成立的)。基於這個假設,spring在transaction創建時,會用ThreadLocal把創建這個事務的jdbc connection綁定到當前thread,接下來在事務的整個生命周期中都會從ThreadLocal中獲取同一個jdbc connection。

我們看一下詳細調用過程

  • TransactionSynchronizationManager負責從ThreadLocal中存取jdbc connection
  • 創建事務的時候會通過dataSource.getConnection()獲取一個新的jdbc connection,然后綁定到ThreadLocal
  • 在業務代碼中執行sql時,通過DataSourceUtils.getConnection()從ThreadLocal中獲取當前事務的jdbc connection, 然后在該jdbc connection上執行sql
  • commit和rollback事務時,從ThreadLocal中獲取當前事務的jdbc connection,然后對該jdbc connection進行commit和rollback

對spring jdbc的事務處理有了了解后,我們來看mybatis是如何通過spring處理事務的。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
  <property name="dataSource" ref="dataSource" />
  <property name="transactionFactory">
    <bean class="org.apache.ibatis.spring.transaction.SpringManagedTransactionFactory" />
  </property> 
</bean>

<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
  <constructor-arg index="0" ref="sqlSessionFactory" />
</bean>
  • mybatis-spring依賴DataSourceTransactionManager來處理事務,並沒有創建自己的PlatformTransactionManager實現。
  • mybatis通過SqlSessionFactoryBuilder創建SqlSessionFactory,而mybatis-spring通過SqlSessionFactoryBean創建SqlSessionFactory。
  • 配置使用SpringManagedTransactionFactory來創建MyBatis的Transaction實現SpringManagedTransaction
  • 配置使用SqlSessionTemplate代替通過SqlSessionFactory.openSession()獲取SqlSession

然后看其調用過程

可以看到mybatis-spring處理事務的主要流程和spring jdbc處理事務並沒有什么區別,都是通過DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事務的生命周期管理,而且jdbc connection的創建也是通過DataSourceTransactionManager.getTransaction()完成,mybatis並沒有參與其中,mybatis只是在執行sql時通過DataSourceUtils.getConnection()獲得當前thread的jdbc connection,然后在其上執行sql。

下面結合代碼來看

<SqlSessionUtils>:  

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {

    notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
    notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);

    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);

    SqlSession session = sessionHolder(executorType, holder);
    if (session != null) {
      return session;
    }

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Creating a new SqlSession");
    }

    session = sessionFactory.openSession(executorType);

    registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);

    return session;
  }


  private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
      PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
    SqlSessionHolder holder;
    if (TransactionSynchronizationManager.isSynchronizationActive()) {
      Environment environment = sessionFactory.getConfiguration().getEnvironment();

      if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
        if (LOGGER.isDebugEnabled()) {
          LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
        }

        holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
        TransactionSynchronizationManager.bindResource(sessionFactory, holder);
        TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        holder.requested();
      } else {
        if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
          }
        } else {
          throw new TransientDataAccessResourceException(
              "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
        }
      }
    } else {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
      }
    }
}

執行sql時調用sqlSessionTemplate的insert,update,delete方法,sqlSessionTemplate是DefaultSqlSession的一個代理類,它通過SqlSessionUtils.getSqlSession()試圖從ThreadLocal獲取當前事務所使用的SqlSession。如果是第一次獲取時會調用SqlSessionFactory.openSession()創建一個SqlSession並綁定到ThreadLocal,同時還會通過TransactionSynchronizationManager注冊一個SqlSessionSynchronization。

<SqlSessionSynchronization>:

 public void beforeCommit(boolean readOnly) {
      // Connection commit or rollback will be handled by ConnectionSynchronization or
      // DataSourceTransactionManager.
      // But, do cleanup the SqlSession / Executor, including flushing BATCH statements so
      // they are actually executed.
      // SpringManagedTransaction will no-op the commit over the jdbc connection
      // TODO This updates 2nd level caches but the tx may be rolledback later on! 
      if (TransactionSynchronizationManager.isActualTransactionActive()) {
        try {
          if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Transaction synchronization committing SqlSession [" + this.holder.getSqlSession() + "]");
          }
          this.holder.getSqlSession().commit();
        } catch (PersistenceException p) {
          if (this.holder.getPersistenceExceptionTranslator() != null) {
            DataAccessException translated = this.holder
                .getPersistenceExceptionTranslator()
                .translateExceptionIfPossible(p);
            if (translated != null) {
              throw translated;
            }
          }
          throw p;
        }
      }

SqlSessionSynchronization是一個事務生命周期的callback接口,mybatis-spring通過SqlSessionSynchronization在事務提交和回滾前分別調用DefaultSqlSession.commit()和DefaultSqlSession.rollback()

<BaseExecutor>:

public void commit(boolean required) throws SQLException {
    if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
    clearLocalCache();
    flushStatements();
    if (required) {
      transaction.commit();
    }
  }

  public void rollback(boolean required) throws SQLException {
    if (!closed) {
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {
          transaction.rollback();
        }
      }
    }
  }

  public void clearLocalCache() {
    if (!closed) {
      localCache.clear();
      localOutputParameterCache.clear();
    }
  }
<SpringManagedTransaction>:

this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);  

  public void commit() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Committing JDBC Connection [" + this.connection + "]");
      }
      this.connection.commit();
    }
  }

  public void rollback() throws SQLException {
    if (this.connection != null && !this.isConnectionTransactional && !this.autoCommit) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Rolling back JDBC Connection [" + this.connection + "]");
      }
      this.connection.rollback();
    }
  }
<DataSourceUtils>:

    /**
     * Determine whether the given JDBC Connection is transactional, that is,
     * bound to the current thread by Spring's transaction facilities.
     * @param con the Connection to check
     * @param dataSource the DataSource that the Connection was obtained from
     * (may be {@code null})
     * @return whether the Connection is transactional
     */
    public static boolean isConnectionTransactional(Connection con, DataSource dataSource) {
        if (dataSource == null) {
            return false;
        }
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        return (conHolder != null && connectionEquals(conHolder, con));
    }

這里的DefaultSqlSession只會進行一些自身緩存的清理工作,並不會真正提交事務給數據庫,原因是這里的DefaultSqlSession使用的Transaction實現為SpringManagedTransaction,SpringManagedTransaction在提交事務前會檢查當前事務是否應該由spring控制,如果是,則不會自己提交事務,而將提交事務的任務交給spring,所以DefaultSqlSession並不會自己處理事務。

<SpringManagedTransaction>: 

 public Connection getConnection() throws SQLException {
    if (this.connection == null) {
      openConnection();
    }
    return this.connection;
  }

  /**
   * Gets a connection from Spring transaction manager and discovers if this
   * {@code Transaction} should manage connection or let it to Spring.
   * <p>
   * It also reads autocommit setting because when using Spring Transaction MyBatis
   * thinks that autocommit is always false and will always call commit/rollback
   * so we need to no-op that calls.
   */
  private void openConnection() throws SQLException {
    this.connection = DataSourceUtils.getConnection(this.dataSource);
    this.autoCommit = this.connection.getAutoCommit();
    this.isConnectionTransactional = DataSourceUtils.isConnectionTransactional(this.connection, this.dataSource);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug(
          "JDBC Connection ["
              + this.connection
              + "] will"
              + (this.isConnectionTransactional ? " " : " not ")
              + "be managed by Spring");
    }
  }

DefaultSqlSession執行sql時,會通過SpringManagedTransaction調用DataSourceUtils.getConnection()從ThreadLocal中獲取jdbc connection並在其上執行sql。

總結:mybatis-spring處理事務的主要流程和spring jdbc處理事務並沒有什么區別,都是通過DataSourceTransactionManager的getTransaction(), rollback(), commit()完成事務的生命周期管理,而且jdbc connection的創建也是通過DataSourceTransactionManager.getTransaction()完成,mybatis並沒有參與其中,mybatis只是在執行sql時通過DataSourceUtils.getConnection()獲得當前thread的jdbc connection,然后在其上執行sql。

mybatis-spring做的最主要的事情是:

  1. 在SqlSession執行sql時通過用SpringManagedTransaction代替mybatis的JdbcTransaction,讓SqlSession從spring的ThreadLocal中獲取jdbc connection。
  2. 通過注冊事務生命周期callback接口SqlSessionSynchronization,讓SqlSession有機會在spring管理的事務提交或回滾時清理自己的內部緩存。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM