深入理解MyBatis(三)--運行源碼解析及延遲加載

GitHub:https://github.com/JDawnF

一、運行源碼解析

先看一下Mybatis的Dao實現類例子,如下:

A、 輸入流的關閉

在輸入流對象使用完畢後,不用手工進行流的關閉。因爲在輸入流被使用完畢後,SqlSessionFactoryBuilder 對象的 build()方法會自動將輸入流關閉。

//SqlSessionFactoryBuilder.java
 public SqlSessionFactory build(InputStream inputStream) {
   return build(inputStream, null, null);
 }
 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
     try {
       XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
       return build(parser.parse());
     } catch (Exception e) {
       throw ExceptionFactory.wrapException("Error building SqlSession.", e);
     } finally {
       ErrorContext.instance().reset();
       try { // 關閉輸入流
         inputStream.close();
       } catch (IOException e) {
         // Intentionally ignore. Prefer previous error.
       }
     }
   }

B、 SqlSession 的創建

SqlSession 接口對象用於執行持久化操作。一個 SqlSession 對應着一次數據庫會話,一 次會話以 SqlSession 對象的創建開始,以 SqlSession 對象的關閉結束。

SqlSession 接口對象是線程不安全的,所以每次數據庫會話結束前,需要馬上調用其 close()方法,將其關閉。再次需要會話,再次創建。而在關閉時會判斷當前的 SqlSession 是否被提交:若沒有被提交,則會執行回滾後關閉;若已被提交,則直接將 SqlSession 關閉。 所以,SqlSession 無需手工回滾。

主要是一些增刪改查的方法。

SqlSession 對象的創建,需要使用 SqlSessionFactory 接口對象的 openSession()方法。 SqlSessionFactory 接口對象是一個重量級對象(系統開銷大的對象),是線程安全的,所以一個應用只需要一個該對象即可。創建 SqlSession 需要使用 SqlSessionFactory 接口的的 openSession()方法。

  • openSession(true):創建一個有自動提交功能的 SqlSession

  • openSession(false):創建一個非自動提交功能的 SqlSession,需手動提交

  • openSession():同 openSession(false) ,即無參的openSession方法默認false是autoCommit的值

SqlSessionFactory 接口的實現類爲 DefaultSqlSessionFactory。


 
// SqlSessionFactory.java
 public interface SqlSessionFactory {
   SqlSession openSession();
     // 多個openSession方法
   Configuration getConfiguration();
 }
 // DefaultSqlSessionFactory.java
 public SqlSession openSession() {
     // false是autoCommit的值,表示關閉事務的自動提交功能
     return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
  }
 //autoCommit表示是否自動提交事務
 private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
     Transaction tx = null;
     try {
         //讀取Mybatis的主配置文件
       final Environment environment = configuration.getEnvironment();
         // 獲取事務管理器transcationManager,比如配置文件中的JDBC
       final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
       tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 創建執行器,傳入的是事務和執行器類型(SIMPLE, REUSE, BATCH)
       final Executor executor = configuration.newExecutor(tx, execType);
       return new DefaultSqlSession(configuration, executor, autoCommit);
     } catch (Exception e) {
       closeTransaction(tx); // may have fetched a connection so lets call close()
       throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
     } finally {
       ErrorContext.instance().reset();
     }
   }
 //DefaultSqlSession.java
 // 所謂創建SqlSession就是對一個dirty這個變量進行初始化,即是否爲髒數據的意思
 public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
     // 對成員變量進行初始化
     this.configuration = configuration;
     this.executor = executor;
     this.dirty = false;     //  這個變量爲false表示現在DB中的數據還未被修改
     this.autoCommit = autoCommit;
   }

從以上源碼可以看到,無參的 openSession()方法,將事務的自動提交直接賦值爲 false。而所謂創建 SqlSession,就是加載了主配置文件,創建了一個執行器對象(將來用於執行映射文件中的 SQL 語句),初始化了一個 DB 數據被修改的標誌變量 dirty,關閉了事務的自動提交功能。

C、 增刪改的執行

對於 SqlSession 的 insert()、delete()、update()方法,其底層均是調用執行了 update()方法,只要對數據進行了增刪改,那麼dirty就會變爲true,表示數據被修改了。

 // DefaultSqlSession.java
 public int insert(String statement, Object parameter) {
     return update(statement, parameter);
   }
 public int delete(String statement, Object parameter) {
     return update(statement, parameter);
   }
 public int update(String statement, Object parameter) {
     try {
       dirty = true;     //這裏要開始修改數據了,所以要將dirty改爲true,表示此時是髒數據
       // statement是獲取映射文件中制定的sql語句,即mapper映射文件中的sql id
       MappedStatement ms = configuration.getMappedStatement(statement);
       return executor.update(ms, wrapCollection(parameter));
     } catch (Exception e) {
       throw ExceptionFactory.wrapException("Error updating database.  Cause: " + e, e);
     } finally {
       ErrorContext.instance().reset();
     }
   }

從以上源碼可知,無論執行增、刪還是改,均是對數據進行修改,均將 dirty 變量設置爲了 true,且在獲取到映射文件中指定 id 的 SQL 語句後,由執行器 executor 執行。

D、 SqlSession 的提交 commit()

// DefaultSqlSession.java
public void commit() {
  commit(false);
}
public void commit(boolean force) {
    try {
        // 執行提交
      executor.commit(isCommitOrRollbackRequired(force));
      dirty = false;	// 提交之後把dirty設置爲false,表示數據未修改
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error committing transaction.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }
// 提交還是回滾
/**當autoCommit爲true時,返回false;
   當autoCommit爲false,dirty爲true時,返回true;
   當autoCommit爲false,dirty爲false時,如果force爲true則返回true,爲false則返回false
   在這裏根據上面方法傳過來的參數值,autoCommit爲false,所以!false==true,dirty爲true,force爲	        	false,所以isCommitOrRollbackRequired返回true。
*/
private boolean isCommitOrRollbackRequired(boolean force) {
    return (!autoCommit && dirty) || force;
  }
// CachingExecutor.java
// required根據上面的值是爲true
public void commit(boolean required) throws SQLException {
    delegate.commit(required);
    tcm.commit();
  }
//BaseExecutor.java
public void commit(boolean required) throws SQLException {
    if (closed) throw new ExecutorException("Cannot commit, transaction is already closed");
    clearLocalCache();
    flushStatements();
    if (required) {	// 根據上面返回的結果,required爲true,提交事務
      transaction.commit();
    }
  }

由以上代碼可知,執行 SqlSession 的無參 commit()方法,最終會將事務進行提交。

E、 SqlSession 的關閉

//DefaultSqlSession.java
public void close() {
  try {
      // 如果執行了commit方法,那麼這裏返回的是false,即close方法中傳入的是false
    executor.close(isCommitOrRollbackRequired(false));
    dirty = false;
  } finally {
    ErrorContext.instance().reset();
  }
}
// 這裏的force爲false,autoCommit在最開始的openSession方法中傳入的是爲false,dirty在commit之後,而在commit方法中,將dirty設置爲false了,所以這裏dirty是false,所以這裏整體返回的是false
private boolean isCommitOrRollbackRequired(boolean force) {
    return (!autoCommit && dirty) || force;
  }
//BaseExecutor.java
public void close(boolean forceRollback) {
    try {
      try {
          // 根據上面傳入的值,forceRollback爲false
        rollback(forceRollback);
      } finally {	// 最後要確認事務關閉,如果前面執行了增刪改查方法,說明提交了事務,所以事務不爲空
        if (transaction != null) transaction.close();
      }
    } catch (SQLException e) {
      // Ignore.  There's nothing that can be done at this point.
      log.warn("Unexpected exception on closing transaction.  Cause: " + e);
    } finally {	//釋放各種資源,並將關閉標誌closed重置爲true
      transaction = null;
      deferredLoads = null;
      localCache = null;
      localOutputParameterCache = null;
      closed = true;
    }
  }
// 根據上面傳進來的值,required爲false
public void rollback(boolean required) throws SQLException {
    if (!closed) {	// 此時還未關閉,所以closed爲false,這裏!closed爲true
      try {
        clearLocalCache();
        flushStatements(true);
      } finally {
        if (required) {		// required爲false,不會回滾事務
          transaction.rollback();
        }
      }
    }
  }

從以上代碼分析可知,在 SqlSession 進行關閉時,如果執行了commit,那麼不會回滾事務;如果沒有執行commit方法,那麼就會回滾事務,那麼數據不會插入到數據庫。所以,對於MyBatis 程序,無需通過顯式地對 SqlSession 進行回滾,達到事務回滾的目的。

二、延遲加載

MyBatis 中的延遲加載,也稱爲懶加載,是指在進行關聯查詢時,按照設置延遲規則推 遲對關聯對象的 select 查詢。延遲加載可以有效的減少數據庫壓力。 需要注意的是,MyBatis 的延遲加載只是對關聯對象的查詢有遲延設置,對於主加載對象都是直接執行查詢語句的。

Mybatis 僅支持 association 關聯對象和 collection 關聯集合對象的延遲加載。其中,association 指的就是一對一,collection 指的就是一對多查詢

它的原理是,使用 CGLIB 或 Javassist( 默認 ) 創建目標對象的代理對象。當調用代理對象的延遲加載屬性的 getting 方法時,進入攔截器方法。比如調用 a.getB().getName() 方法,進入攔截器的 invoke(...) 方法,發現 a.getB() 需要延遲加載時,那麼就會單獨發送事先保存好的查詢關聯 B 對象的 SQL ,把 B 查詢上來,然後調用a.setB(b) 方法,於是 a 對象 b屬性就有值了,接着完成a.getB().getName() 方法的調用。這就是延遲加載的基本原理。

當然了,不光是 Mybatis,幾乎所有的包括 Hibernate 在內,支持延遲加載的原理都是一樣的。

1.關聯對象加載時機

MyBatis 根據對關聯對象查詢的 select 語句的執行時機,分爲三種類型:直接加載、侵 入式延遲加載與深度延遲加載。

  • 直接加載:執行完對主加載對象的 select 語句,馬上執行對關聯對象的 select 查詢。

  • 侵入式延遲:執行對主加載對象的查詢時,不會執行對關聯對象的查詢。但當要訪問主加載對象的詳情時,就會馬上執行關聯對象的 select 查詢。即對關聯對象的查詢執行, 侵入到了主加載對象的詳情訪問中。也可以這樣理解:將關聯對象的詳情侵入到了主加 載對象的詳情中,即將關聯對象的詳情作爲主加載對象的詳情的一部分出現了。

  • 深度延遲:執行對主加載對象的查詢時,不會執行對關聯對象的查詢。訪問主加載對象 的詳情時也不會執行關聯對象的 select 查詢。只有當真正訪問關聯對象的詳情時,纔會 執行對關聯對象的 select 查詢。

需要注意的是,延遲加載的應用要求,關聯對象的查詢與主加載對象的查詢必須是分別進行的 select 語句,不能是使用多表連接所進行的 select 查詢。因爲,多表連接查詢,其實 質是對一張表的查詢,對由多個表連接後形成的一張表的查詢。會一次性將多張表的所有信 息查詢出來。

MyBatis 中對於延遲加載設置,可以應用到一對一、一對多、多對一、多對多的所有關 聯關係查詢中。

2.直接加載

修改主配置文件:在主配置文件的<properties/>與<typeAliases/>標籤之間,添加<settings/>標籤,用於完 成全局參數設置。

延遲加載的相關參數名稱及取值:

全局屬性 lazyLoadingEnabled 的值只要設置爲 false,那麼,對於關聯對象的查詢,將採 用直接加載。即在查詢過主加載對象後,會馬上查詢關聯對象。

lazyLoadingEnabled 的默認值爲 false,即直接加載。

3.深度延遲加載

修改主配置文件的<settings/>,將延遲加載開關 lazyLoadingEnabled 開啓(置爲 true), 將侵入式延遲加載開關 aggressiveLazyLoading 關閉(置爲 false)。

4.侵入式延遲加載

修改主配置文件的<settings/>,將延遲加載開關 lazyLoadingEnabled 開啓(置爲 true), 將侵入式延遲加載開關 aggressiveLazyLoading 也開啓(置爲 true,默認爲 true)。

該延遲策略使關聯對象的數據侵入到了主加載對象的數據中,所以稱爲 侵入式延遲加載。 需要注意的是,該延遲策略也是一種延遲加載,需要在延遲加載開關 lazyLoadingEnabled 開啓時纔會起作用。若 lazyLoadingEnabled 爲 false,則 aggressiveLazyLoading 無論取何值, 均不起作用。

5.延遲加載策略總結

參照:動力節點

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章