1. 程式人生 > >mybatis 延遲加載

mybatis 延遲加載

提高 tar wid rep name obj 觀察 word 嵌套

本文我們研究mybatis的嵌套查詢和延遲加載。

1.預備知識
resultMap是mybatis裏的一個高級功能。通過利用association和collection,可以做到將多個表關聯到到一起,但又不用寫JOIN這種復雜SQL,有點類似於hibernate、JPA。
如果不熟悉resultMap的話,可以讀一下官方的文檔。

2.官方例子
學習最好的方法就是看例子
我這裏下載了官方的mybatis3.3.0-SNAPSHOT源碼,借用裏面一個測試程序來跟蹤一下嵌套查詢和延遲加載這兩個特性。

找到org.apache.ibatis.submitted.cglib_lazy_error包,裏面有兩個測試程序,
CglibNPETest是測試嵌套查詢的,沒有用延遲加載。
CglibNPELazyTest則用了延遲加載。

2.1 表結構和測試數據
CreateDB.sql

Sql代碼 技術分享
  1. create table person (
  2. id int,
  3. firstName varchar(100),
  4. lastName varchar(100),
  5. parent int DEFAULT NULL
  6. );
  7. INSERT INTO person (id, firstName, lastName, parent) VALUES (1, ‘John sr.‘, ‘Smith‘, null);
  8. INSERT INTO person (id, firstName, lastName, parent) VALUES (2, ‘John‘, ‘Smith‘, 1);
  9. INSERT INTO person (id, firstName, lastName, parent) VALUES (3, ‘John jr.‘, ‘Smith‘, 2);



表結構我們只要關心parent字段就可以了,是說這個人的父親是誰。然後插入3條記錄,3的父親是2,2的父親是1

2.2 Bean定義
Person.java

Java代碼 技術分享
  1. public class Person {
  2. private Long id;
  3. private String firstName;
  4. private String lastName;
  5. private Person parent;
  6. }



2.3 Mapper定義
Person.xml

Xml代碼 技術分享
  1. <resultMap id="personMap" type="Person">
  2. <id property="id" column="Person_id"/>
  3. <result property="firstName" column="Person_firstName"/>
  4. <result property="lastName" column="Person_lastName"/>
  5. <association property="parent" column="Person_parent" select="selectById"/>
  6. </resultMap>
  7. <select id="selectById" resultMap="personMap" parameterType="int">
  8. SELECT
  9. <include refid="columns"/>
  10. FROM Person
  11. WHERE id = #{id,jdbcType=INTEGER}
  12. </select>



可以看到要關聯父子,沒有采用寫JOIN語句的方法,而是在resultMap裏定義了一個association,然後最後的select="selectById"表明要用一個嵌套查詢來查得父親記錄。

3.測試準備
為了看的清楚一點,我們打開DEBUG的log,最簡單的可以采用STDOUT_LOGGING,將日誌輸出到控制臺。
兩個文件,ibatisConfig.xml是CglibNPETest用的,ibatisConfigLazy.xml是CglibNPELazyTest用的。

ibatisConfig.xml

Xml代碼 技術分享
  1. <settings>
  2. <setting name="logImpl" value="STDOUT_LOGGING"/>
  3. </settings>



ibatisConfigLazy.xml

Xml代碼 技術分享
  1. <settings>
  2. <setting name="proxyFactory" value="CGLIB"/>
  3. <setting name="lazyLoadingEnabled" value="true"/>
  4. <setting name="logImpl" value="STDOUT_LOGGING"/>
  5. </settings>




4.嵌套查詢測試
CglibNPETest.testAncestorAfterQueryingParents方法
斷點分別設在這2句話上

Java代碼 技術分享
  1. Person expectedAncestor = personMapper.selectById(1);
  2. Person person = personMapper.selectById(3);



先運行selectById(1),觀察日誌

Txt代碼 技術分享
  1. ==> Preparing: SELECT Person.id AS Person_id, Person.firstName AS Person_firstName, Person.lastName AS Person_lastName, Person.parent AS Person_parent FROM Person WHERE id = ?
  2. ==> Parameters: 1(Integer)
  3. <== Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
  4. <== Row: 1, John sr., Smith, null
  5. <== Total: 1


mybatis發了1條SQL取得id為1的記錄。

然後運行selectById(3),觀察日誌

Txt代碼 技術分享
  1. ==> Preparing: SELECT Person.id AS Person_id, Person.firstName AS Person_firstName, Person.lastName AS Person_lastName, Person.parent AS Person_parent FROM Person WHERE id = ?
  2. ==> Parameters: 3(Integer)
  3. <== Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
  4. <== Row: 3, John jr., Smith, 2
  5. ====> Preparing: SELECT Person.id AS Person_id, Person.firstName AS Person_firstName, Person.lastName AS Person_lastName, Person.parent AS Person_parent FROM Person WHERE id = ?
  6. ====> Parameters: 2(Integer)
  7. <==== Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
  8. <==== Row: 2, John, Smith, 1
  9. <==== Total: 1
  10. <== Total: 1


可以看到mybatis采用了發2條SQL的方法來實現這個嵌套查詢的功能。先 select 3, 再 select 2,同時註意下圖右上角person的類型的確是如假包換的Person型。
技術分享

進一步深入,一步步跟蹤進去,調用堆棧如圖所示,這張圖大家不要看錯,調用順序是從下往上的,所以請從下往上看。
技術分享
最下面的$Proxy5.selectById想必大家一定都知道了,表明了personMapper是一個代理,這就是為什麽我們只需要定義mapper的接口,而不需要實現的原因了,mybatis用JDK的動態代理幫我們實現了。

接下來這段調用流程的入口點我們可以看到是CachingExecutor.query,目的是為了取得id=3的記錄

CachingExecutor.query
-->SimpleExecutor.query
-->SimpleExecutor.prepareStatement
-->RoutingStatementHandler.query
-->PreparedStatementHandler.query

取得記錄後,交給DefaultResultSetHandler處理,要做的事情是將Resultset轉換成一個List
----->DefaultResultSetHandler.<E> handleResultSets
----->DefaultResultSetHandler.handleResultSet
----->DefaultResultSetHandler.handleRowValues
----->DefaultResultSetHandler.handleRowValuesForSimpleResultMap
----->DefaultResultSetHandler.getRowValue

怎麽轉,肯定先要創建bean,然後再把屬性一個個設上去咯,這些都是用反射來做到的。
-------->DefaultResultSetHandler.createResultObject
-------->DefaultResultSetHandler.createResultObject
先用反射new一個Person對象

但是如果是嵌套查詢且要延遲加載,則用cglib或javassist生成一個代理,這個後文再說。
-------->ProxyFactory.createProxy

----->DefaultResultSetHandler.applyAutomaticMappings
----->DefaultResultSetHandler.applyPropertyMappings

開始把屬性一個個設上去咯
----->DefaultResultSetHandler.getPropertyMappingValue
----->typeHandler.getResult
如果是普通的值就用相應的typeHandler來從resultset中取得值

然後就是parent這種有嵌套查詢的則調用此嵌套查詢方法
----->getNestedQueryMappingValue
-------->lazyLoader.addLoader
有延遲加載則addLoader,這個後文再說。
-------->ResultLoader.loadResult
沒有延遲加載則立即加載
----------->ResultLoader.selectList
----------->CachingExecutor.query

這裏的CachingExecutor.query,目的是為了取得id=2的記錄
然後看到了沒,這是一個遞歸調用,這樣又轉回去了,一個輪回。。。。。。這樣就可以不斷遞歸取到父親、爺爺、曾祖父咯。。。。。。
不過mybatis還是做了一點優化的,看到日誌裏只發了2條SQL取3和2兩條記錄,而1這條記錄因為之前就取過了嘛,已經在緩存裏了,所以沒必要重復取了。當然這也是防死循環的一個方法了,我們看下官方文檔的說明:

引用 本地緩存機制(Local Cache)防止循環引用(circular references)和加速重復嵌套查詢。默認值為 SESSION,這種情況下會緩存一個會話中執行的所有查詢。


要註意的是這個本地緩存是一級緩存。而二級緩存的處理則是通過CachingExecutor處理的。
不理解一級緩存、二級緩存的,可參考這篇文章 MyBatis 緩存機制深度解剖 / 自定義二級緩存 。

5.延遲加載測試(cglib)
CglibNPELazyTest.testAncestorAfterQueryingParents方法
同樣的斷點分別設在這2句話上

Java代碼 技術分享
  1. Person expectedAncestor = personMapper.selectById(1);
  2. Person person = personMapper.selectById(3);



我們略過第一句話,執行selectById(3)以後觀察日誌,發現mybatis只發了1條SQL取得3這條記錄

Txt代碼 技術分享
  1. ==> Preparing: SELECT Person.id AS Person_id, Person.firstName AS Person_firstName, Person.lastName AS Person_lastName, Person.parent AS Person_parent FROM Person WHERE id = ?
  2. ==> Parameters: 3(Integer)
  3. <== Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
  4. <== Row: 3, John jr., Smith, 2
  5. <== Total: 1



而當調用了下面的話person.getParent()以後,mybatis才去發另一條SQL取得2這條記錄

Txt代碼 技術分享
  1. ==> Preparing: SELECT Person.id AS Person_id, Person.firstName AS Person_firstName, Person.lastName AS Person_lastName, Person.parent AS Person_parent FROM Person WHERE id = ?
  2. ==> Parameters: 2(Integer)
  3. <== Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
  4. <== Row: 2, John, Smith, 1
  5. <== Total: 1



這便是延遲加載的效果了,和hibernate如出一轍啊。如何做到的呢,進一步跟蹤。
DefaultResultSetHandler.getRowValue
-------->DefaultResultSetHandler.createResultObject
但是如果是嵌套查詢且要延遲加載,則用cglib或javassist生成一個代理。
-------->ProxyFactory.createProxy
看圖,這次生成的person是一個冒牌的person,它的類型是Person$$EnhancerByCGLIB$$bdd8787e類型的,是由cglib創建的一個代理
技術分享

然後就是parent這種有嵌套查詢的則調用此嵌套查詢方法
----->getNestedQueryMappingValue
-------->lazyLoader.addLoader
有延遲加載則addLoader,把要延遲加載的屬性記到ResultLoaderMap裏(一個哈希表)

然後當我們調用person.getParent()以後,圖中可清楚的看到這個方法被攔截啦!
技術分享

Person$$EnhancerByCGLIB$$bdd8787e.getParent
-->CglibProxyFactory$EnhancedResultObjectProxyImpl.intercept
-->ResultLoaderMap.load
-->ResultLoaderMap$LoadPair.load
-------->ResultLoader.loadResult
立即加載
----------->ResultLoader.selectList
----------->CachingExecutor.query

看到了沒,又轉回CachingExecutor.query這個入口點了,所以就可以發另1條SQL來取得id=2這條記錄了

6.延遲加載測試(javassist)
這次我們把cglib換成javassist試一下
ibatisConfigLazy.xml

Xml代碼 技術分享
  1. <settings>
  2. <setting name="proxyFactory" value=""JAVASSIST""/>
  3. <setting name="lazyLoadingEnabled" value="true"/>
  4. <setting name="logImpl" value="STDOUT_LOGGING"/>
  5. </settings>


還是用和cglib相同的方法斷點調試,看圖,這次生成的person的類型是Person_$$_jvst844_0類型的,是由javassist創建的一個代理
技術分享

然後當我們調用person.getParent()以後,圖中可清楚的看到這個方法被攔截啦!
技術分享

Person_$$_jvst844_0.getParent
-->JavassistProxyFactory$EnhancedResultObjectProxyImpl.invoke
然後後面就和cglib一模一樣了。

7.resultMap與resultType比較
resultMap雖然強大,從設計上看很牛叉,但是筆者這裏還是提一下自己的觀點,筆者覺得一般情況下用用resultType足夠了,沒必要用resultMap

resultMap
優點:使用嵌套查詢的話([email protected])多表不用寫JOIN這種復雜SQL。
缺點:“N+1 查詢問題”,會導致成百上千的 SQL 語句被執行,不過可以通過延遲加載一部分解決這個性能問題。另一種根治的方法就是用嵌套的resultMap,不過這樣寫出來的resultMap就更復雜了。

resultType
優點:自己寫多表關聯的SQL比較踏實,可以做SQL的性能調優。
缺點:導致大量的DTO需要創建,不過可以考慮將多個SQL的select出來的字段做一個最大的並集,這些SQL共用一個DTO


8.總結
mybatis的嵌套查詢和延遲加載,雖然大家可能不會用到這個功能(至少筆者覺得不實用),但是設計思想是可以借鑒的。提供了cglib,javassist兩種方法來實現延遲加載,這和hibernate的延遲加載如出一轍啊!另外一級緩存和二級緩存的使用,也是和hibernate思想一致!裏面用到的一些技術,如反射,動態代理,字節碼(cglib,javassist)則是java的基礎,另加許多設計模式的運用,使得mybatis源碼顯得比較優雅,大家品讀mybatis源碼對自己一定是一個提高。

另外,筆者在github上fork了一個mybatis源碼中文註釋版,方便大家學習交流。

mybatis 延遲加載