1. 程式人生 > >MyBatis原始碼解析(三)——Transaction事務模組(轉)

MyBatis原始碼解析(三)——Transaction事務模組(轉)

1、回顧

  之前介紹了Environment環境類,這其實是一個單例類,在MyBatis執行開啟後只會存在一個唯一的環境例項,雖然我們可以在Configuration配置檔案中配置多個環境,但是專案執行中只會存在其中的一個,一般專案會存在開發環境和測試環境、生產環境三大環境,其是否可以設定到配置檔案中,在開發時使用開發環境,測試時使用測試環境,正式運營時可以使用生產環境。

  之前還提到Environment類中有三個欄位,除了id之外,TransactionFactory和DataSource都是比較複雜的模組,這一次我們介紹Transaction模組(即事務模組)。

2、事務模組

  事務模組位於org.apache.ibatis.transaction

包,這個包內的類均是事務相關的類:

    org.apache.ibatis.transaction
  -----org.apache.ibatis.transaction.jdbc
  ----------JdbcTransaction.java
  ----------JdbcTransactionFactory.java
  -----org.apache.ibatis.transaction.managed
  ----------ManagedTransaction.java
  ----------ManagedTransactionFactory.java
  -----Transaction.java
  -----TransactionException.java
  -----TransactionFactory.java

  從上面的類結構中也能看出來,MyBatis的事務模組採用的是工廠模式。

2.1 事務介面

  位於org.apache.ibatis.transaction包的Transaction和TransactionFactory都是介面類。

  Transaction是事務介面,其中定義了四個方法:

      commit()-事務提交

      rollBack()-事務回滾

      close()-關閉資料庫連線

      getConnection()-獲取資料庫連線

  如下程式碼所示:

package org.apache.ibatis.transaction;
import java.sql.Connection;
import java.sql.SQLException;
/**
 * 事務,包裝了一個Connection, 包含commit,rollback,close方法
 * 在 MyBatis 中有兩種事務管理器型別(也就是 type=”[JDBC|MANAGED]”):  
 */
public interface Transaction {
  Connection getConnection() throws SQLException;
  void commit() throws SQLException;
  void rollback() throws SQLException;
  void close() throws SQLException;
}

  TransactionFactory是事務工廠介面,其中定義了三個方法:

      setProperties(Properties props)-設定屬性

      newTransaction(Connection conn)-建立事務例項

      newTransaction(DataSource dataSource,TransactionIsolationLevel level,boolean autoCommit)-建立事務例項

  如下程式碼所示:

package org.apache.ibatis.transaction;
import java.sql.Connection;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.session.TransactionIsolationLevel;
/**
 * 事務工廠
 */
public interface TransactionFactory {
  //設定屬性
  void setProperties(Properties props);
  //根據Connection建立Transaction
  Transaction newTransaction(Connection conn);
  //根據資料來源和事務隔離級別建立Transaction
  Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel level, boolean autoCommit);
}

  Transacrion介面定義的目的就是為了對具體的事務型別進行抽象,便於擴充套件;TransactionFactory與其一樣,是對事務工廠的抽象,同樣便於具體型別的事務工廠的擴充套件實現。

2.2 MyBatis事務型別

  說到這裡,就不得不提到MyBatis裡內建的兩種事務型別及對應的事務工廠了,還記得在上一文中給出的environment配置資訊,有這麼一句:

 <transactionManager type="JDBC"/>

  這裡的<transactionManager>標籤就是用於定義專案所使用的事務型別,具體的型別由type屬性來指定,此處指定使用“JDBC”型別事務,當然MyBatis還提供了另外一種“MANAGED”型事務。

    ---JDBC

    ---MANAGED

  這裡的“JDBC”和“MANAGED”是在Configuration配置類的類型別名註冊器中註冊的別名,其對應的類分別是:JdbcTransactionFactory.class和ManagedTransactionFactory.class。具體的配置如下所述:

 typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
 typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);

  上面的程式碼是在Configuration類的無參構造器中定義的,這裡拿來僅用於展示,具體說明以後會介紹。

  這裡提一句:類型別名註冊器額原理就是將別名與具體的類型別以鍵值對的方式儲存到一個HashMap中,這樣只要知道別名(鍵),就可以從Map中得到對應的值(Class型別),很簡單!

  現在只要知道MyBatis能夠根據你在配置檔案中設定的事務型別,直接找到對應的事務工廠類就行了。

  下面對上面提到的兩種事務型別進行解讀。

      ---JDBC事務模型:JdbcTransaction

      ---MANAFED事務模型:ManagedTransaction

  二者的不同之處在於:前者是直接使用JDK提供的JDBC來管理事務的各個環節:提交、回滾、關閉等操作,而後者則什麼都不做,那麼後者有什麼意義呢,當然很重要。

  當我們單獨使用MyBatis來構建專案時,我們要在Configuration配置檔案中進行環境(environment)配置,在其中要設定事務型別為JDBC,意思是說MyBatis被單獨使用時就需要使用JDBC型別的事務模型,因為在這個模型中定義了事務的各個方面,使用它可以完成事務的各項操作。而MANAGED型別的事務模型其實是一個託管模型,也就是說它自身並不實現任何事務功能,而是託管出去由其他框架來實現,你可能還不明白,這個事務的具體實現就交由如Spring之類的框架來實現,而且在使用SSM整合框架後已經不再需要單獨配置環境資訊(包括事務配置與資料來源配置),因為在在整合jar包(mybatis-spring.jar)中擁有覆蓋mybatis裡面的這部分邏輯的程式碼,實際情況是即使你顯式設定了相關配置資訊,系統也會視而不見......

  託管的意義顯而易見,正是為整合而設。

  我們學習MyBatis的目的正是由於其靈活性和與Spring等框架的無縫整合的能力,所以有關JDBC事務模組的內容明顯不再是MyBatis功能中的重點,也許只有在單獨使用MyBatis的少量系統中才會使用到。

2.3 JDBC事務模型

  雖然JDBC事務型別很少使用到,但是作為MyBatis不可分割的一部分,我們還是需要進行一定的瞭解,JDBC事務的實現是對JDK中提供的JDBC事務模組的再封裝,以適用於MyBatis環境。

  MyBatis中的JDBC事務模組包括兩個部分,分別為JDBC事務工廠和JDBC事務,整個事務模組採用的是抽象工廠模式,那麼對應於每一項具體的事務處理模組必然擁有自己的事務工廠,事務模組例項通過事務工廠來建立(事務工廠將具體的事務例項的建立封裝起來)。

  首先我們來看JDBC事務工廠:JdbcTransactionFactory

  JdbcTransactionFactory繼承自TransactionFactory介面,實現了其中的所有方法。分別為一個設定屬性的方法和兩個新建事務例項的方法(引數不同),內容很簡單,作用也很簡單。

  其中setProperties()方法用於設定屬性,這個方法在XMLConfigBuilder中解析事務標籤時呼叫,用於解析事務標籤的下級屬性標籤<property>(一般情況下我們並不會進行設定,但是如果我們進行了設定,那就會覆蓋MyBatis中的預設設定)之後將其設定到建立的事務例項中。然而針對JDBC事務模型,在事務工廠的設定屬性方法中沒有任何執行程式碼,也就說明JDBC事務模組並不支援設定屬性的功能,即使你在配置檔案中設定的一些資訊,也不會有任何作用。

  那麼這個方法有什麼用呢?前面提到,這個設定用於覆蓋預設設定,只是JDBC事務模組並不支援而已,但並不代表別的事務模型不支援,同時這個方法也可用於功能擴充套件。

  另外兩個方法顯而易見,就是用於建立JDBC事務例項的生產方法,只是引數不同,方法的過載而已。其中一個生產方法僅需傳遞一個例項Connection,這代表一個數據庫連線。而另一個方法需要傳遞三個引數(DataSource、TransactionIsolationLevel、boolean),其實這對應於MyBatis中SqlSession的兩種生產方式,其引數與這裡一一對應,這部分內容以後介紹,此處不再贅述。

  然後我們來看看JDBC事務類:JdbcTransaction

  其中有四個引數:

protected Connection connection;
protected DataSource dataSource;
protected TransactionIsolationLevel level;
protected boolean autoCommmit;

  這四個引數分別對應事務工廠中的兩個生產方法中的總共四個引數,對應的在事務類中定義了兩個構造器,構造例項的同時進行賦值:

public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {
    dataSource = ds;
    level = desiredLevel;
    autoCommmit = desiredAutoCommit;
  }
  public JdbcTransaction(Connection connection) {
    this.connection = connection;
  }

  其次在該類中實現了Transaction介面,實現了其中的四個方法,三個功能性方法,和一個獲取資料庫連線的方法。三個功能方法分別是提交、回滾和關閉。

@Override
  public void commit() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Committing JDBC Connection [" + connection + "]");
      }
      connection.commit();
    }
  }

  @Override
  public void rollback() throws SQLException {
    if (connection != null && !connection.getAutoCommit()) {
      if (log.isDebugEnabled()) {
        log.debug("Rolling back JDBC Connection [" + connection + "]");
      }
      connection.rollback();
    }
  }

  @Override
  public void close() throws SQLException {
    if (connection != null) {
      resetAutoCommit();
      if (log.isDebugEnabled()) {
        log.debug("Closing JDBC Connection [" + connection + "]");
      }
      connection.close();
    }
  }

  通過觀察這三個方法,可以發現,其中都使用了connection來進行具體操作,因此這些方法使用的前提就是先獲取connection資料庫連線,Connection的獲取使用getConnection()方法

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

  在上面的方法中呼叫了openConnection()方法:


protected void openConnection() throws SQLException {
	if (log.isDebugEnabled()) {
	  log.debug("Opening JDBC Connection");
	}
	connection = dataSource.getConnection();
	if (level != null) {
	  connection.setTransactionIsolation(level.getLevel());
	}
	setDesiredAutoCommit(autoCommmit);
}

  可見connection是從資料來源dataSource中獲取的,最後會呼叫setDesiredAutoCommit()方法:

protected void setDesiredAutoCommit(boolean desiredAutoCommit) {
    try {
      if (connection.getAutoCommit() != desiredAutoCommit) {
        if (log.isDebugEnabled()) {
          log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(desiredAutoCommit);
      }
    } catch (SQLException e) {
      // Only a very poorly implemented driver would fail here,
      // and there's not much we can do about that.
      throw new TransactionException("Error configuring AutoCommit.  "
          + "Your driver may not support getAutoCommit() or setAutoCommit(). "
          + "Requested setting: " + desiredAutoCommit + ".  Cause: " + e, e);
    }
}

  這個方法的目的就是為connection中的自動提交賦值(真或假)。

  這麼看來,我們建立事務例項所提供的三個引數就是為connection服務的,其中DataSource是用來獲取Connection例項的,而TransactionIsolationLevel(事務級別)和boolean(自動提交)是用來填充connection的,通過三個引數我們獲得了一個圓滿的Connection例項,然後我們就可以使用這個例項來進行事務操作:提交、回滾、關閉。

2.4 關於自動提交

  在之前的程式碼中我們能看到在關閉操作之前呼叫了一個方法:resetAutoCommit():

protected void resetAutoCommit() {
    try {
      if (!connection.getAutoCommit()) {
        // MyBatis does not call commit/rollback on a connection if just selects were performed.
        // Some databases start transactions with select statements
        // and they mandate a commit/rollback before closing the connection.
        // A workaround is setting the autocommit to true before closing the connection.
        // Sybase throws an exception here.
        if (log.isDebugEnabled()) {
          log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");
        }
        connection.setAutoCommit(true);
      }
    } catch (SQLException e) {
      log.debug("Error resetting autocommit to true "
          + "before closing the connection.  Cause: " + e);
    }
}

  這裡相對自動提交做個解說,如果設定自動提交為真,那麼資料庫將會將每一個SQL語句當做一個事務來執行,為了將多條SQL當做一個事務進行提交,必須將自動提交設定為false,然後進行手動提交。一般在我們的專案中,都需要將自動提交設定為false,即將自動提交關閉,使用手動提交

  這個方法中通過對connection例項中的自動提交設定(真或假)進行判斷,如果為false,表明不執行自動提交,則復位,重新將其設定為true。(自動提交的預設值為true)這個操作執行在connection關閉之前。可以看做是連線關閉之前的復位操作。

2.5 問題

  在JdbcTransaction中提供的兩個構造器中以Connection為引數的構造器額作用是什麼呢?

  我們需要自動組裝一個完整的Connection,以其為引數來生產一個事務例項。這用在什麼場景中呢?