終結篇:MyBatis原理深入解析(三)

原文鏈接:https://www.jianshu.com/p/cf32582169db

11 MyBatis一級緩存實現#

11.1 什麼是一級緩存? 爲什麼使用一級緩存?##

每當我們使用MyBatis開啓一次和數據庫的會話,MyBatis會創建出一個SqlSession對象表示一次數據庫會話。

在對數據庫的一次會話中,我們有可能會反覆地執行完全相同的查詢語句,如果不採取一些措施的話,每一次查詢都會查詢一次數據庫,而我們在極短的時間內做了完全相同的查詢,那麼它們的結果極有可能完全相同,由於查詢一次數據庫的代價很大,這有可能造成很大的資源浪費。

爲了解決這一問題,減少資源的浪費,MyBatis會在表示會話的SqlSession對象中建立一個簡單的緩存,將每次查詢到的結果結果緩存起來,當下次查詢的時候,如果判斷先前有個完全一樣的查詢,會直接從緩存中直接將結果取出,返回給用戶,不需要再進行一次數據庫查詢了。

如下圖所示,MyBatis會在一次會話的表示----一個SqlSession對象中創建一個本地緩存(local cache),對於每一次查詢,都會嘗試根據查詢的條件去本地緩存中查找是否在緩存中,如果在緩存中,就直接從緩存中取出,然後返回給用戶;否則,從數據庫讀取數據,將查詢結果存入緩存並返回給用戶

MyBatis一級緩存簡單示意圖

對於會話(Session)級別的數據緩存,我們稱之爲一級數據緩存,簡稱一級緩存。

11.2 MyBatis中的一級緩存是怎樣組織的?(即SqlSession中的緩存是怎樣組織的?)##

由於MyBatis使用SqlSession對象表示一次數據庫的會話,那麼,對於會話級別的一級緩存也應該是在SqlSession中控制的

實際上, SqlSession只是一個MyBatis對外的接口,SqlSession將它的工作交給了Executor執行器這個角色來完成,負責完成對數據庫的各種操作。當創建了一個SqlSession對象時,MyBatis會爲這個SqlSession對象創建一個新的Executor執行器,而緩存信息就被維護在這個Executor執行器中,MyBatis將緩存和對緩存相關的操作封裝成了Cache接口中。SqlSession、Executor、Cache之間的關係如下列類圖所示:

SqlSession、Executor、Cache之間的關係

如上述的類圖所示,Executor接口的實現類BaseExecutor中擁有一個Cache接口的實現類PerpetualCache,則對於BaseExecutor對象而言,它將使用PerpetualCache對象維護緩存

綜上,SqlSession對象、Executor對象、Cache對象之間的關係如下圖所示:

SqlSession對象、Executor對象、Cache對象之間的關係

由於Session級別的一級緩存實際上就是使用PerpetualCache維護的,那麼PerpetualCache是怎樣實現的呢?

PerpetualCache實現原理其實很簡單,其內部就是通過一個簡單的HashMap<k,v> 來實現的,沒有其他的任何限制。如下是PerpetualCache的實現代碼:

package org.apache.ibatis.cache.impl;  
  
import java.util.HashMap;  
import java.util.Map;  
import java.util.concurrent.locks.ReadWriteLock;  
  
import org.apache.ibatis.cache.Cache;  
import org.apache.ibatis.cache.CacheException;  
  
/** 
 * 使用簡單的HashMap來維護緩存 
 * @author Clinton Begin 
 */  
public class PerpetualCache implements Cache {  
  
  private String id;  
  
  private Map<Object, Object> cache = new HashMap<Object, Object>();  
  
  public PerpetualCache(String id) {  
    this.id = id;  
  }  
  
  public String getId() {  
    return id;  
  }  
  
  public int getSize() {  
    return cache.size();  
  }  
  
  public void putObject(Object key, Object value) {  
    cache.put(key, value);  
  }  
  
  public Object getObject(Object key) {  
    return cache.get(key);  
  }  
  
  public Object removeObject(Object key) {  
    return cache.remove(key);  
  }  
  
  public void clear() {  
    cache.clear();  
  }  
  
  public ReadWriteLock getReadWriteLock() {  
    return null;  
  }  
  
  public boolean equals(Object o) {  
    if (getId() == null) throw new CacheException("Cache instances require an ID.");  
    if (this == o) return true;  
    if (!(o instanceof Cache)) return false;  
  
    Cache otherCache = (Cache) o;  
    return getId().equals(otherCache.getId());  
  }  
  
  public int hashCode() {  
    if (getId() == null) throw new CacheException("Cache instances require an ID.");  
    return getId().hashCode();  
  }  
  
} 

11.3 一級緩存的生命週期有多長?##

  1. MyBatis在開啓一個數據庫會話時,會創建一個新的SqlSession對象,SqlSession對象中會有一個新的Executor對象,Executor對象中持有一個新的PerpetualCache對象;當會話結束時,SqlSession對象及其內部的Executor對象還有PerpetualCache對象也一併釋放掉
  1. 如果SqlSession調用了close()方法,會釋放掉一級緩存PerpetualCache對象,一級緩存將不可用;
  1. 如果SqlSession調用了clearCache(),會清空PerpetualCache對象中的數據,但是該對象仍可使用;
  1. **SqlSession中執行了任何一個update操作(update()、delete()、insert()) **,都會清空PerpetualCache對象的數據,但是該對象可以繼續使用;

一級緩存的生命週期

11.4 SqlSession 一級緩存的工作流程##

  1. 對於某個查詢,根據statementId,params,rowBounds來構建一個key值,根據這個key值去緩存Cache中取出對應的key值存儲的緩存結果;
  2. 判斷從Cache中根據特定的key值取的數據數據是否爲空,即是否命中;
  3. 如果命中,則直接將緩存結果返回;
  4. 如果沒命中:
    4.1 去數據庫中查詢數據,得到查詢結果;
    4.2 將key和查詢到的結果分別作爲key,value對存儲到Cache中;
    4.3 將查詢結果返回;
  5. 結束。

SqlSession查詢工作時序圖

11.5 Cache接口的設計以及CacheKey的定義##

如下圖所示,MyBatis定義了一個org.apache.ibatis.cache.Cache接口作爲其Cache提供者的SPI(Service Provider Interface),所有的MyBatis內部的Cache緩存,都應該實現這一接口。MyBatis定義了一個PerpetualCache實現類實現了Cache接口,實際上,在SqlSession對象裏的Executor對象內維護的Cache類型實例對象,就是PerpetualCache子類創建的

MyBatis內部還有很多Cache接口的實現,一級緩存只會涉及到這一個PerpetualCache子類,Cache的其他實現將會放到二級緩存中介紹。

MyBatis內部Cache接口

我們知道,Cache最核心的實現其實就是一個Map,將本次查詢使用的特徵值作爲key,將查詢結果作爲value存儲到Map中。現在最核心的問題出現了:怎樣來確定一次查詢的特徵值?換句話說就是:怎樣判斷某兩次查詢是完全相同的查詢?也可以這樣說:如何確定Cache中的key值?

MyBatis認爲,對於兩次查詢,如果以下條件都完全一樣,那麼就認爲它們是完全相同的兩次查詢:

  1. **傳入的 statementId **

  2. 查詢時要求的結果集中的結果範圍 (結果的範圍通過rowBounds.offset和rowBounds.limit表示)

  3. 這次查詢所產生的最終要傳遞給JDBC java.sql.Preparedstatement的Sql語句字符串(boundSql.getSql() )

  4. 傳遞給java.sql.Statement要設置的參數值

現在分別解釋上述四個條件:

  1. 傳入的statementId,對於MyBatis而言,你要使用它,必須需要一個statementId,它代表着你將執行什麼樣的Sql
  1. MyBatis自身提供的分頁功能是通過RowBounds來實現的,它通過rowBounds.offset和rowBounds.limit來過濾查詢出來的結果集,這種分頁功能是基於查詢結果的再過濾,而不是進行數據庫的物理分頁;
  1. 由於MyBatis底層還是依賴於JDBC實現的,那麼,對於兩次完全一模一樣的查詢,MyBatis要保證對於底層JDBC而言,也是完全一致的查詢才行。而對於JDBC而言,兩次查詢,只要傳入給JDBC的SQL語句完全一致,傳入的參數也完全一致,就認爲是兩次查詢是完全一致的。
  1. 上述的第3個條件正是要求保證傳遞給JDBC的SQL語句完全一致;第4條則是保證傳遞給JDBC的參數也完全一致;即3、4兩條MyBatis最本質的要求就是:調用JDBC的時候,傳入的SQL語句要完全相同,傳遞給JDBC的參數值也要完全相同

綜上所述,CacheKey由以下條件決定:statementId + rowBounds + 傳遞給JDBC的SQL + 傳遞給JDBC的參數值

  1. CacheKey的創建

對於每次的查詢請求,Executor都會根據傳遞的參數信息以及動態生成的SQL語句,將上面的條件根據一定的計算規則,創建一個對應的CacheKey對象。

我們知道創建CacheKey的目的,就兩個:

  1. 根據CacheKey作爲key,去Cache緩存中查找緩存結果;

  2. 如果查找緩存命中失敗,則通過此CacheKey作爲key,將從數據庫查詢到的結果作爲value,組成key,value對存儲到Cache緩存中;

CacheKey的構建被放置到了Executor接口的實現類BaseExecutor中,定義如下:

/** 
   * 所屬類:  org.apache.ibatis.executor.BaseExecutor 
   * 功能   :   根據傳入信息構建CacheKey 
   */  
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {  
      if (closed) throw new ExecutorException("Executor was closed.");  
      CacheKey cacheKey = new CacheKey();  
      //1.statementId  
      cacheKey.update(ms.getId());  
      //2. rowBounds.offset  
      cacheKey.update(rowBounds.getOffset());  
      //3. rowBounds.limit  
      cacheKey.update(rowBounds.getLimit());  
      //4. SQL語句  
      cacheKey.update(boundSql.getSql());  
      //5. 將每一個要傳遞給JDBC的參數值也更新到CacheKey中  
      List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();  
      TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();  
      for (int i = 0; i < parameterMappings.size(); i++) { // mimic DefaultParameterHandler logic  
          ParameterMapping parameterMapping = parameterMappings.get(i);  
          if (parameterMapping.getMode() != ParameterMode.OUT) {  
              Object value;  
              String propertyName = parameterMapping.getProperty();  
              if (boundSql.hasAdditionalParameter(propertyName)) {  
                  value = boundSql.getAdditionalParameter(propertyName);  
              } else if (parameterObject == null) {  
                  value = null;  
              } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {  
                  value = parameterObject;  
              } else {  
                  MetaObject metaObject = configuration.newMetaObject(parameterObject);  
                  value = metaObject.getValue(propertyName);  
              }  
              //將每一個要傳遞給JDBC的參數值也更新到CacheKey中  
              cacheKey.update(value);  
          }  
      }  
      return cacheKey;  
} 
  1. CacheKey的hashcode生成算法

剛纔已經提到,Cache接口的實現,本質上是使用的HashMap<k,v>,而構建CacheKey的目的就是爲了作爲HashMap<k,v>中的key值而HashMap是通過key值的hashcode 來組織和存儲的,那麼,構建CacheKey的過程實際上就是構造其hashCode的過程。下面的代碼就是CacheKey的核心hashcode生成算法,感興趣的話可以看一下:

public void update(Object object) {  
      if (object != null && object.getClass().isArray()) {  
          int length = Array.getLength(object);  
          for (int i = 0; i < length; i++) {  
              Object element = Array.get(object, i);  
              doUpdate(element);  
          }  
      } else {  
          doUpdate(object);  
      }  
}  
 
private void doUpdate(Object object) {  
 
      //1. 得到對象的hashcode;    
      int baseHashCode = object == null ? 1 : object.hashCode();  
      //對象計數遞增  
      count++;  
      checksum += baseHashCode;  
      //2. 對象的hashcode 擴大count倍  
      baseHashCode *= count;  
      //3. hashCode * 拓展因子(默認37)+拓展擴大後的對象hashCode值  
      hashcode = multiplier * hashcode + baseHashCode;  
      updateList.add(object);  
}  

MyBatis認爲的完全相同的查詢,不是指使用sqlSession查詢時傳遞給算起來Session的所有參數值完完全全相同,你只要保證statementId,rowBounds,最後生成的SQL語句,以及這個SQL語句所需要的參數完全一致就可以了。

11.6 一級緩存的性能分析##

  1. MyBatis對會話(Session)級別的一級緩存設計的比較簡單,就簡單地使用了HashMap來維護,並沒有對HashMap的容量和大小進行限制

讀者有可能就覺得不妥了:如果我一直使用某一個SqlSession對象查詢數據,這樣會不會導致HashMap太大,而導致 java.lang.OutOfMemoryError錯誤啊?讀者這麼考慮也不無道理,不過MyBatis的確是這樣設計的。

MyBatis這樣設計也有它自己的理由:

a. 一般而言SqlSession的生存時間很短。一般情況下使用一個SqlSession對象執行的操作不會太多,執行完就會消亡;

b. 對於某一個SqlSession對象而言,只要執行update操作(update、insert、delete),都會將這個SqlSession對象中對應的一級緩存清空掉,所以一般情況下不會出現緩存過大,影響JVM內存空間的問題;

c. 可以手動地釋放掉SqlSession對象中的緩存。

  1. 一級緩存是一個粗粒度的緩存,沒有更新緩存和緩存過期的概念

MyBatis的一級緩存就是使用了簡單的HashMap,MyBatis只負責將查詢數據庫的結果存儲到緩存中去, 不會去判斷緩存存放的時間是否過長、是否過期,因此也就沒有對緩存的結果進行更新這一說了。

根據一級緩存的特性,在使用的過程中,我認爲應該注意:

  1. 對於數據變化頻率很大,並且需要高時效準確性的數據要求,我們使用SqlSession查詢的時候,要控制好SqlSession的生存時間,SqlSession的生存時間越長,它其中緩存的數據有可能就越舊,從而造成和真實數據庫的誤差;同時對於這種情況,用戶也可以手動地適時清空SqlSession中的緩存

  2. 對於只執行、並且頻繁執行大範圍的select操作的SqlSession對象,SqlSession對象的生存時間不應過長。

12 MyBatis二級緩存實現#

MyBatis的二級緩存是Application級別的緩存,它可以提高對數據庫查詢的效率,以提高應用的性能。

12.1 MyBatis的緩存機制整體設計以及二級緩存的工作模式##

MyBatis緩存機制示意圖

如上圖所示,當開一個會話時,一個SqlSession對象會使用一個Executor對象來完成會話操作,MyBatis的二級緩存機制的關鍵就是對這個Executor對象做文章。如果用戶配置了"cacheEnabled=true",那麼MyBatis在爲SqlSession對象創建Executor對象時,會對Executor對象加上一個裝飾者:CachingExecutor,這時SqlSession使用CachingExecutor對象來完成操作請求。CachingExecutor對於查詢請求,會先判斷該查詢請求在Application級別的二級緩存中是否有緩存結果,如果有查詢結果,則直接返回緩存結果;如果緩存中沒有,再交給真正的Executor對象來完成查詢操作,之後CachingExecutor會將真正Executor返回的查詢結果放置到緩存中,然後在返回給用戶。

二級緩存工作模式

CachingExecutor是Executor的裝飾者,以增強Executor的功能,使其具有緩存查詢的功能,這裏用到了設計模式中的裝飾者模式,CachingExecutor和Executor的接口的關係如下類圖所示:

使用裝飾者模式實現CachingExecutor

12.2 MyBatis二級緩存的劃分##

MyBatis並不是簡單地對整個Application就只有一個Cache緩存對象,它將緩存劃分的更細,即是Mapper級別的,即每一個Mapper都可以擁有一個Cache對象,具體如下:

  1. 爲每一個Mapper分配一個Cache緩存對象(使用<cache>節點配置)

MyBatis將Application級別的二級緩存細分到Mapper級別,即對於每一個Mapper.xml,如果在其中使用了<cache> 節點,則MyBatis會爲這個Mapper創建一個Cache緩存對象,如下圖所示:

MyBatis會爲這個Mapper創建一個Cache緩存對象

注:上述的每一個Cache對象,都會有一個自己所屬的namespace命名空間,並且會將Mapper的 namespace作爲它們的ID;

  1. 多個Mapper共用一個Cache緩存對象(使用<cache-ref>節點配置)

如果你想讓多個Mapper公用一個Cache的話,你可以使用<cache-ref namespace="">節點,來指定你的這個Mapper使用到了哪一個Mapper的Cache緩存

讓多個Mapper公用一個Cache

12.3 使用二級緩存,必須要具備的條件##

MyBatis對二級緩存的支持粒度很細,它會指定某一條查詢語句是否使用二級緩存

雖然在Mapper中配置了<cache>,並且爲此Mapper分配了Cache對象,這並不表示我們使用Mapper中定義的查詢語句查到的結果都會放置到Cache對象之中,我們必須指定Mapper中的某條選擇語句是否支持緩存,即如下所示,在<select> 節點中配置useCache="true",Mapper纔會對此Select的查詢支持緩存特性,否則,不會對此Select查詢,不會經過Cache緩存。如下所示,Select語句配置了useCache="true",則表明這條Select語句的查詢會使用二級緩存。

<select id="selectByMinSalary" resultMap="BaseResultMap" parameterType="java.util.Map" useCache="true">

總之,要想使某條Select查詢支持二級緩存,你需要保證:

  1. MyBatis支持二級緩存的總開關:全局配置變量參數 cacheEnabled=true

  2. 該select語句所在的Mapper,配置了<cache> 或<cached-ref>節點,並且有效;

  3. 該select語句的參數 useCache=true

12.4 一級緩存和二級緩存的使用順序##

請注意,如果你的MyBatis使用了二級緩存,並且你的Mapper和select語句也配置使用了二級緩存,那麼在執行select查詢的時候,MyBatis會先從二級緩存中取輸入,其次纔是一級緩存,即MyBatis查詢數據的順序是:二級緩存 ———> 一級緩存 ——> 數據庫

12.5 二級緩存實現的選擇##

MyBatis對二級緩存的設計非常靈活,它自己內部實現了一系列的Cache緩存實現類,並提供了各種緩存刷新策略如LRU,FIFO等等;另外,MyBatis還允許用戶自定義Cache接口實現,用戶是需要實現org.apache.ibatis.cache.Cache接口,然後將Cache實現類配置在<cache type="">節點的type屬性上即可;除此之外,MyBatis還支持跟第三方內存緩存庫如Memecached的集成,總之,使用MyBatis的二級緩存有三個選擇:

  1. MyBatis自身提供的緩存實現

  2. 用戶自定義的Cache接口實現

  3. 跟第三方內存緩存庫的集成

12.6 MyBatis自身提供的二級緩存的實現##

MyBatis自身提供了豐富的,並且功能強大的二級緩存的實現,它擁有一系列的Cache接口裝飾者,可以滿足各種對緩存操作和更新的策略。

MyBatis定義了大量的Cache的裝飾器來增強Cache緩存的功能,如下類圖所示。

對於每個Cache而言,都有一個容量限制,MyBatis各供了各種策略來對Cache緩存的容量進行控制,以及對Cache中的數據進行刷新和置換。MyBatis主要提供了以下幾個刷新和置換策略:

LRU:(Least Recently Used),最近最少使用算法,即如果緩存中容量已經滿了,會將緩存中最近最少被使用的緩存記錄清除掉,然後添加新的記錄;

FIFO:(First in first out),先進先出算法,如果緩存中的容量已經滿了,那麼會將最先進入緩存中的數據清除掉;

Scheduled:指定時間間隔清空算法,該算法會以指定的某一個時間間隔將Cache緩存中的數據清空;

MyBatis自身提供的二級緩存的實現

13 如何細粒度地控制你的MyBatis二級緩存#

13.1 一個關於MyBatis的二級緩存的實際問題##

現有AMapper.xml中定義了對數據庫表 ATable 的CRUD操作,BMapper定義了對數據庫表BTable的CRUD操作;

假設 MyBatis 的二級緩存開啓,並且 AMapper 中使用了二級緩存,AMapper對應的二級緩存爲ACache;

除此之外,AMapper 中還定義了一個跟BTable有關的查詢語句,類似如下所述:

<select id="selectATableWithJoin" resultMap="BaseResultMap" useCache="true">  
      select * from ATable left join BTable on ....  
</select>

執行以下操作:

  1. 執行AMapper中的"selectATableWithJoin" 操作,此時會將查詢到的結果放置到AMapper對應的二級緩存ACache中;
  2. 執行BMapper中對BTable的更新操作(update、delete、insert)後,BTable的數據更新;
  3. 再執行1完全相同的查詢,這時候會直接從AMapper二級緩存ACache中取值,將ACache中的值直接返回;

好,問題就出現在第3步上:

由於AMapper的“selectATableWithJoin” 對應的SQL語句需要和BTable進行join查找,而在第 2 步BTable的數據已經更新了,但是第 3 步查詢的值是第 1 步的緩存值,已經極有可能跟真實數據庫結果不一樣,即ACache中緩存數據過期了!

總結來看,就是:

對於某些使用了 join連接的查詢,如果其關聯的表數據發生了更新,join連接的查詢由於先前緩存的原因,導致查詢結果和真實數據不同步;

從MyBatis的角度來看,這個問題可以這樣表述:

對於某些表執行了更新(update、delete、insert)操作後,如何去清空跟這些表有關聯的查詢語句所造成的緩存;

13.2 當前MyBatis二級緩存的工作機制##

MyBatis二級緩存的工作機制

MyBatis二級緩存的一個重要特點:即鬆散的Cache緩存管理和維護

一個Mapper中定義的增刪改查操作只能影響到自己關聯的Cache對象。如上圖所示的Mapper namespace1中定義的若干CRUD語句,產生的緩存只會被放置到相應關聯的Cache1中,即Mapper namespace2,namespace3,namespace4 中的CRUD的語句不會影響到Cache1。

可以看出,Mapper之間的緩存關係比較鬆散,相互關聯的程度比較弱。

現在再回到上面描述的問題,如果我們將AMapper和BMapper共用一個Cache對象,那麼,當BMapper執行更新操作時,可以清空對應Cache中的所有的緩存數據,這樣的話,數據不是也可以保持最新嗎?

確實這個也是一種解決方案,不過,它會使緩存的使用效率變的很低!AMapper和BMapper的任意的更新操作都會將共用的Cache清空,會頻繁地清空Cache,導致Cache實際的命中率和使用率就變得很低了,所以這種策略實際情況下是不可取的。

最理想的解決方案就是:

對於某些表執行了更新(update、delete、insert)操作後,如何去清空跟這些表有關聯的查詢語句所造成的緩存;這樣,就是以很細的粒度管理MyBatis內部的緩存,使得緩存的使用率和準確率都能大大地提升。

13.3 mybatis-enhanced-cache插件的設計和工作原理##

該插件主要由兩個構件組成:EnhancedCachingExecutor和EnhancedCachingManager。源碼地址:https://github.com/LuanLouis/mybatis-enhanced-cache

EnhancedCachingExecutor是針對於Executor的攔截器,攔截Executor的幾個關鍵的方法;EnhancedCachingExecutor主要做以下幾件事:

  1. 每當有Executor執行query操作時,
    1.1 記錄下該查詢StatementId和CacheKey,然後將其添加到EnhancedCachingManager中;
    1.2 記錄下該查詢StatementId和此StatementId所屬Mapper內的Cache緩存對象引用,添加到EnhancedCachingManager中;

  2. 每當Executor執行了update操作時,將此update操作的StatementId傳遞給EnhancedCachingManager,讓EnhancedCachingManager根據此update的StatementId的配置,去清空指定的查詢語句所產生的緩存;

另一個構件:EnhancedCachingManager,它也是本插件的核心,它維護着以下幾樣東西:

  1. 整個MyBatis的所有查詢所產生的CacheKey集合(以statementId分類);

  2. 所有的使用過了的查詢的statementId 及其對應的Cache緩存對象的引用;

  3. update類型的StatementId和查詢StatementId集合的映射,用於當Update類型的語句執行時,根據此映射決定應該清空哪些查詢語句產生的緩存;

如下圖所示:

mybatis-enhanced-cache插件設計工作原理

原理很簡單,就是 當執行了某個update操作時,根據配置信息去清空指定的查詢語句在Cache中所產生的緩存數據。

13.4 mybatis-enhanced-cache 插件的使用實例##

  1. 配置MyBatis配置文件
<plugins>  
      <plugin interceptor="org.luanlouis.mybatis.plugin.cache.EnhancedCachingExecutor">  
         <property name="dependency" value="dependencys.xml"/>  
         <property name="cacheEnabled" value="true"/>  
      </plugin>  
</plugins>

其中,<property name="dependency"> 中的value屬性是 StatementId之間的依賴關係的配置文件路徑

  1. 配置StatementId之間的依賴關係
<?xml version="1.0" encoding="UTF-8"?>  
<dependencies>  
    <statements>  
        <statement id="com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey">  
            <observer id="com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments" />  
        </statement>  
    </statements>  
</dependencies>  

<statement>節點配置的是更新語句的statementId,其內的子節點<observer> 配置的是當更新語句執行後,應當清空緩存的查詢語句的StatementId。子節點<observer>可以有多個。

如上的配置,則說明,如果
"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"
更新語句執行後,由
“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”
語句所產生的放置在Cache緩存中的數據都都會被清空。

  1. 配置DepartmentsMapper.xml 和EmployeesMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >  
<mapper namespace="com.louis.mybatis.dao.DepartmentsMapper" >     
    <cache></cache>  
    <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Department" >  
        <id column="DEPARTMENT_ID" property="departmentId" jdbcType="DECIMAL" />  
        <result column="DEPARTMENT_NAME" property="departmentName" jdbcType="VARCHAR" />  
        <result column="MANAGER_ID" property="managerId" jdbcType="DECIMAL" />  
        <result column="LOCATION_ID" property="locationId" jdbcType="DECIMAL" />  
    </resultMap>  
    <sql id="Base_Column_List" >  
        DEPARTMENT_ID, DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID  
    </sql>  
    <update id="updateByPrimaryKey" parameterType="com.louis.mybatis.model.Department" >  
        update HR.DEPARTMENTS  
        set DEPARTMENT_NAME = #{departmentName,jdbcType=VARCHAR},  
        MANAGER_ID = #{managerId,jdbcType=DECIMAL},  
        LOCATION_ID = #{locationId,jdbcType=DECIMAL}  
        where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}  
    </update>  
    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Integer" >  
        select   
        <include refid="Base_Column_List" />  
        from HR.DEPARTMENTS  
        where DEPARTMENT_ID = #{departmentId,jdbcType=DECIMAL}  
    </select>  
</mapper> 
<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">  
<mapper namespace="com.louis.mybatis.dao.EmployeesMapper">  
    <cache eviction="LRU" flushInterval="100000" size="10000"/>   
    <resultMap id="BaseResultMap" type="com.louis.mybatis.model.Employee">  
        <id column="EMPLOYEE_ID" jdbcType="DECIMAL" property="employeeId" />  
        <result column="FIRST_NAME" jdbcType="VARCHAR" property="firstName" />  
        <result column="LAST_NAME" jdbcType="VARCHAR" property="lastName" />  
        <result column="EMAIL" jdbcType="VARCHAR" property="email" />  
        <result column="PHONE_NUMBER" jdbcType="VARCHAR" property="phoneNumber" />  
        <result column="HIRE_DATE" jdbcType="DATE" property="hireDate" />  
        <result column="JOB_ID" jdbcType="VARCHAR" property="jobId" />  
        <result column="SALARY" jdbcType="DECIMAL" property="salary" />  
        <result column="COMMISSION_PCT" jdbcType="DECIMAL" property="commissionPct" />  
        <result column="MANAGER_ID" jdbcType="DECIMAL" property="managerId" />  
        <result column="DEPARTMENT_ID" jdbcType="DECIMAL" property="departmentId" />  
     </resultMap>   
     <sql id="Base_Column_List">  
        EMPLOYEE_ID, FIRST_NAME, LAST_NAME, EMAIL, PHONE_NUMBER, HIRE_DATE, JOB_ID, SALARY,   
        COMMISSION_PCT, MANAGER_ID, DEPARTMENT_ID  
     </sql>    
     <select id="selectWithDepartments" parameterType="java.lang.Integer" resultMap="BaseResultMap" useCache="true" >  
        select   
        *  
        from HR.EMPLOYEES t left join HR.DEPARTMENTS S ON T.DEPARTMENT_ID = S.DEPARTMENT_ID  
        where EMPLOYEE_ID = #{employeeId,jdbcType=DECIMAL}  
     </select>
</mapper>
  1. 測試代碼:
public class SelectDemo3 {  
   private static final Logger loger = Logger.getLogger(SelectDemo3.class);  
   public static void main(String[] args) throws Exception {  
       InputStream inputStream = Resources.getResourceAsStream("mybatisConfig.xml");  
       SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();  
       SqlSessionFactory factory = builder.build(inputStream);  
         
       SqlSession sqlSession = factory.openSession(true);  
       SqlSession sqlSession2 = factory.openSession(true);  
       //3.使用SqlSession查詢  
       Map<String,Object> params = new HashMap<String,Object>();  
       params.put("employeeId",10);  
       //a.查詢工資低於10000的員工  
       Date first = new Date();  
       //第一次查詢  
       List<Employee> result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);  
       sqlSession.commit();  
       checkCacheStatus(sqlSession);  
       params.put("employeeId", 11);  
       result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);  
       sqlSession.commit();  
       checkCacheStatus(sqlSession);  
       params.put("employeeId", 12);  
       result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);  
       sqlSession.commit();  
       checkCacheStatus(sqlSession);  
       params.put("employeeId", 13);  
       result = sqlSession.selectList("com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments",params);  
       sqlSession.commit();  
       checkCacheStatus(sqlSession);  
       Department department = sqlSession.selectOne("com.louis.mybatis.dao.DepartmentsMapper.selectByPrimaryKey",10);  
       department.setDepartmentName("updated");  
       sqlSession2.update("com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey", department);  
       sqlSession.commit();  
       checkCacheStatus(sqlSession);  
   }      
   public static void checkCacheStatus(SqlSession sqlSession)  
   {  
       loger.info("------------Cache Status------------");  
       Iterator<String> iter = sqlSession.getConfiguration().getCacheNames().iterator();  
       while(iter.hasNext())  
       {  
           String it = iter.next();  
           loger.info(it+":"+sqlSession.getConfiguration().getCache(it).getSize());  
       }  
       loger.info("------------------------------------");     
   } 
}  

結果分析:

從上述的結果可以看出,前四次執行了
“com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments”
語句,EmployeesMapper對應的Cache緩存中存儲的結果緩存有1個增加到4個。

當執行了
"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"
後,EmployeeMapper對應的緩存Cache結果被清空了,即
"com.louis.mybatis.dao.DepartmentsMapper.updateByPrimaryKey"更新語句引起了EmployeeMapper中的
"com.louis.mybatis.dao.EmployeesMapper.selectWithDepartments"
緩存的清空。

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