`
xpenxpen
  • 浏览: 725203 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

mybatis的嵌套查询和延迟加载分析

阅读更多
本文我们研究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
create table person (
  id int,
  firstName varchar(100),
  lastName varchar(100),
  parent int DEFAULT NULL
);

INSERT INTO person (id, firstName, lastName, parent) VALUES (1, 'John sr.', 'Smith', null);
INSERT INTO person (id, firstName, lastName, parent) VALUES (2, 'John', 'Smith', 1);
INSERT INTO person (id, firstName, lastName, parent) VALUES (3, 'John jr.', 'Smith', 2);


表结构我们只要关心parent字段就可以了,是说这个人的父亲是谁。然后插入3条记录,3的父亲是2,2的父亲是1

2.2 Bean定义
Person.java
public class Person {

  private Long id;
  private String firstName;
  private String lastName;
  private Person parent;
}


2.3 Mapper定义
Person.xml

<resultMap id="personMap" type="Person">
    <id property="id" column="Person_id"/>
    <result property="firstName" column="Person_firstName"/>
    <result property="lastName" column="Person_lastName"/>
    <association property="parent" column="Person_parent" select="selectById"/>
</resultMap>

<select id="selectById" resultMap="personMap" parameterType="int">
    SELECT
    <include refid="columns"/>
    FROM Person
    WHERE id = #{id,jdbcType=INTEGER}
</select>


可以看到要关联父子,没有采用写JOIN语句的方法,而是在resultMap里定义了一个association,然后最后的select="selectById"表明要用一个嵌套查询来查得父亲记录。

3.测试准备
为了看的清楚一点,我们打开DEBUG的log,最简单的可以采用STDOUT_LOGGING,将日志输出到控制台。
两个文件,ibatisConfig.xml是CglibNPETest用的,ibatisConfigLazy.xml是CglibNPELazyTest用的。

ibatisConfig.xml
<settings>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>


ibatisConfigLazy.xml
<settings>
    <setting name="proxyFactory" value="CGLIB"/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>



4.嵌套查询测试
CglibNPETest.testAncestorAfterQueryingParents方法
断点分别设在这2句话上
Person expectedAncestor = personMapper.selectById(1);
Person person = personMapper.selectById(3);


先运行selectById(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 = ? 
==> Parameters: 1(Integer)
<==    Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
<==        Row: 1, John sr., Smith, null
<==      Total: 1

mybatis发了1条SQL取得id为1的记录。

然后运行selectById(3),观察日志

==>  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 = ? 
==> Parameters: 3(Integer)
<==    Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
<==        Row: 3, John jr., Smith, 2
====>  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 = ? 
====> Parameters: 2(Integer)
<====    Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
<====        Row: 2, John, Smith, 1
<====      Total: 1
<==      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句话上
Person expectedAncestor = personMapper.selectById(1);
Person person = personMapper.selectById(3);


我们略过第一句话,执行selectById(3)以后观察日志,发现mybatis只发了1条SQL取得3这条记录
==>  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 = ? 
==> Parameters: 3(Integer)
<==    Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
<==        Row: 3, John jr., Smith, 2
<==      Total: 1


而当调用了下面的话person.getParent()以后,mybatis才去发另一条SQL取得2这条记录

==>  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 = ? 
==> Parameters: 2(Integer)
<==    Columns: PERSON_ID, PERSON_FIRSTNAME, PERSON_LASTNAME, PERSON_PARENT
<==        Row: 2, John, Smith, 1
<==      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
<settings>
    <setting name="proxyFactory" value=""JAVASSIST""/>
    <setting name="lazyLoadingEnabled" value="true"/>
    <setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

还是用和cglib相同的方法断点调试,看图,这次生成的person的类型是Person_$$_jvst844_0类型的,是由javassist创建的一个代理


然后当我们调用person.getParent()以后,图中可清楚的看到这个方法被拦截啦!


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

7.resultMap与resultType比较
resultMap虽然强大,从设计上看很牛叉,但是笔者这里还是提一下自己的观点,笔者觉得一般情况下用用resultType足够了,没必要用resultMap

resultMap
优点:使用嵌套查询的话(association@select)多表不用写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源码中文注释版,方便大家学习交流。
  • 大小: 80.1 KB
  • 大小: 57.3 KB
  • 大小: 62.4 KB
  • 大小: 44 KB
  • 大小: 57.8 KB
  • 大小: 41.7 KB
2
0
分享到:
评论
1 楼 jmsyoung 2016-01-26  
请教一下cglib和javaasist的版本,为什么我用延迟加载就报很多exception了

相关推荐

    Mybatis查询延迟加载详解及实例

    Mybatis的查询延迟加载是一种优化策略,用于提高数据查询效率。在默认情况下,Mybatis不会自动启用延迟加载,而是会一次性加载所有关联的数据。这意味着,当你执行一个查询时,如果查询结果包含其他对象的引用,...

    mybatis 的高级关联查询源码

    这次我们将深入探讨 MyBatis 如何实现这种高级关联查询,并通过源码分析来理解其工作原理。 “一对多”关联通常指的是一个实体(如用户)可以拥有多个关联实体(如订单)。在 MyBatis 中,我们可以使用 `...

    MyBatis思维导图.docx

    动态SQL之外,MyBatis还提供了延迟加载(Lazy Loading)功能。默认情况下,当查询一个对象时,其关联的对象并不会立即加载,而是等到真正需要使用时才执行相应的SQL语句,这样可以避免一次性加载大量数据导致的性能...

    Mybatis系列课程-一对一

    Mybatis 是一款流行的 Java 框架,专用于简化数据库操作。它允许开发者将 SQL 查询直接集成到 XML 或注解...从基础的ResultMap配置,到复杂的嵌套查询和关联加载策略,都将逐一探讨,确保你能够熟练掌握这一核心技术。

    基于java的企业级应用开发:MyBatis的关联映射.ppt

    这时,MyBatis的延迟加载机制就能发挥作用,通过配置`lazyLoadingEnabled`和`aggressiveLazyLoading`设置,可以在不增加太多开销的情况下提高查询效率。 开启延迟加载后,关联的对象只有在真正需要时才会被加载,...

    mybatis全局参数.docx

    默认包括 `equals`、`clone`、`hashCode` 和 `toString`,当这些方法被调用时,如果对象的属性尚未加载,MyBatis 将自动加载它们。 通过理解并合理配置这些全局参数,开发者可以根据项目的具体需求调整 MyBatis ...

    MyBatis的27道面试题

    MyBatis支持延迟加载,其原理是按需加载关联对象,而不是在使用时立即加载,这对于关联对象数据量大的情况尤其有用。MyBatis的一级缓存是SqlSession级别的缓存,二级缓存是Mapper级别的缓存,可以被多个SqlSession...

    mybatis之多对多

    9. **延迟加载(Lazy Loading)**:为了提高性能,MyBatis支持延迟加载,即在真正需要多对多关联数据时才进行查询。这可以通过`fetchType="lazy"`属性来实现。 10. **缓存(Cache)**:MyBatis的缓存机制可以帮助...

    mybatis-3-config.dtd mybatis-3-mapper.dtd

    1. **settings**:设置MyBatis全局属性,例如缓存策略、延迟加载、日志级别等。 2. **typeAliases**:定义别名,简化Java类引用,使得在XML配置中可以使用简短的类名。 3. **environments**:配置数据库环境,可以...

    Mybatis关联映射

    MyBatis提供了多种方法来处理这些关联查询,包括嵌套结果、嵌套查询等。 #### 三、关联查询类型详解 ##### 1. 多对一 - **概念**: 在多对一的关系中,多个实体对应一个实体。例如,多个订单可能对应一个客户。 - ...

    MyBatis的关联映射彩色PPT版本.pptx

    为了解决这个问题,MyBatis提供延迟加载机制,通过在核心配置文件中开启`lazyLoadingEnabled`和关闭`aggressiveLazyLoading`,可以在需要时才加载关联对象,从而提高效率。 对于一对多的关系,例如一个部门有多名...

    mybatis一对多性能优化demo

    2. **联合查询(Eager Fetching)**:通过`&lt;select&gt;`标签的`useCache="true"`和`@ResultMap`注解,我们可以执行一次嵌套查询,一次性获取User和其关联的Orders。这种方式减少了数据库访问次数,但增加了单条SQL的...

    Mybatis实现一对一关联查询(Mysql数据库)

    在实际项目中,我们可能还需要考虑性能优化,如使用延迟加载(Lazy Loading)避免不必要的查询,或者根据业务需求选择是否在同一个查询中获取关联数据。总之,正确配置Mybatis的映射文件和Java实体类,可以轻松地...

    Springboot中mybatis表关联映射关系(一对一)

    * `fetchType`:指定在嵌套查询时是否启动延迟加载,有 `lazy` 和 `eager` 两个属性值,默认值为 `lazy`(默认关联映射延迟加载)。 在使用 `&lt;association&gt;` 元素时,需要在 `resultMap` 元素中定义好对应的实体类...

    mybatis多对多配置

    8. **延迟加载(Lazy Loading)**: MyBatis支持延迟加载,即在真正需要关联数据时才执行查询。这可以通过在`&lt;collection&gt;`标签中设置`lazyLoad="true"`实现。 9. **示例代码**:在`mybatisDemo001`项目中,可能包含...

    spring mybatis 3.x 使用图文

    在配置文件中,通常会指定MyBatis的配置文件路径`configLocation`,以及是否启用延迟加载等特性。 #### Mapper映射器与注解优先级 在Spring与MyBatis的整合中,Mapper接口被广泛使用。MyBatis允许通过注解或XML...

    mybatis学习文档资料

    ### MyBatis 学习知识点概述 #### 一、MyBatis 概述与特性 ...以上内容涵盖了MyBatis框架高级映射、查询缓存、与Spring框架整合等多个方面的知识点,旨在帮助开发者深入了解MyBatis的核心技术和应用场景。

    Mybatis复杂映射开发开源架构源码2021.pdf

    Mybatis的版本发展到5.1时,官方提供了更多的功能和改进,比如对延迟加载(懒加载)的支持更加强大,提升了性能和易用性。 在Mybatis中,接口绑定提供了将Mybatis的映射语句和接口方法进行绑定的功能,使得开发者...

    mybatis教程

    最后,从《MyBatis+3+User+Guide+Simplified+Chinese.pdf》中,你可以期待更深入的案例分析和实践指导,帮助你在实际项目中更好地应用MyBatis。 总的来说,这些资源为初学者提供了全面的MyBatis学习路径,从基础...

    mybatis高级映射

    **定义**: 延迟加载是指在需要时才加载关联信息,而非一开始就加载所有信息。 **实现方式**: - **映射文件**: 使用 `&lt;association&gt;` 和 `&lt;collection&gt;` 标签的 `fetchType="lazy"` 属性。 - **配置文件**: 在 `...

Global site tag (gtag.js) - Google Analytics