Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?

MyBatis 支持延迟加载,设置 lazyLoadingEnabled=true 即可。
延迟加载的原理的是调用的时候触发加载,而不是在初始化的时候就加载信息。比如调用 a. getB(). getName(),这个时候发现 a. getB() 的值为 null,此时会单独触发事先保存好的关联 B 对象的 SQL,先查询出来 B,然后再调用 a. setB(b),而这时候再调用 a. getB(). getName() 就有值了,这就是延迟加载的基本原理。

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。

它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。

当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。

1.什么是延迟加载

MyBatis中的延迟加载,也称为懒加载,是指在进行表的关联查询时,按照设置延迟规则推迟对关联对象的select查询。例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,mybatis再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。MyBatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。

注意:延迟加载的应用要求:关联对象的查询与主加载对象的查询必须是分别进行的select语句,不能是使用多表连接所进行的select查询

2.加载时机

mybatis对于延迟加载的时机支持三种形式
直接加载:执行完对主加载对象的 select 语句,马上执行对关联对象的 select 查询。
侵入式延迟: 执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的详情属性时,就会马上执行关联对象的select查询。
深度延迟: 执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象的详情时也不会执行关联对象的select查询。只有当真正访问关联对象的详情时,才会执行对关联对象的 select 查询。

3.延迟加载使用场景

首先我们先思考一个问题,假设:在一对多中,我们有一个用户,他有100个账户。

问题1:在查询用户的时候,要不要把关联的账户查出来?
问题2:在查询账户的时候,要不要把关联的用户查出来?
解答:在查询用户的时候,用户下的账户信息应该是我们什么时候使用,什么时候去查询。在查询账户的时候,账户的所属用户信息应该是随着账户查询时一起查询出来。

在对应的四种表关系中,一对多、多对多通常情况下采用延迟加载,多对一、一对一通常情况下采用立即加载。

理解了延迟加载的特性以后再看Mybatis中如何实现查询方法的延迟加载,在MyBatis 的配置文件中通过设置settings的lazyLoadingEnabled属性为true进行开启全局的延迟加载,通过aggressiveLazyLoading属性开启立即加载。看一下官网的介绍,然后通过一个实例来实现Mybatis的延迟加载,在例子中我们展现一对多表关系情况下,通过实现查询用户信息同时查询出该用户所拥有的账户信息的功能展示一下延迟加载的实现方式以及延迟加载和立即加载的结果的不同之处。

1.用户类以及账户类

  1. public class User implements Serializable{
  2. private Integer id;
  3. private String username;
  4. private Date birthday;
  5. private String sex;
  6. private String address;
  7. private List<Account> accountList;
  8. getset方法省略.....
  9. }
  10. public class Account implements Serializable{
  11. private Integer id;
  12. private Integer uid;
  13. private Double money;
  14. getset方法省略.....
  15. }

注意因为我们是查找用户的同时查找出其所拥有的账户所以我们需要在用户类中增加账户的集合的属性,用来封装返回的结果。

2.在UserDao接口中声明findAll方法

  1. /**
  2. * 查询所有的用户
  3. *
  4. * @return
  5. */
  6. List<User> findAll();

3.在UserDao.xml中配置findAll方法的映射

  1. <resultMap id="userAccountMap" type="com.example.domain.User">
  2. <id property="id" column="id"/>
  3. <result property="username" column="username"/>
  4. <result property="birthday" column="birthday"/>
  5. <result property="sex" column="sex"/>
  6. <result property="address" column="address"/>
  7. <collection property="accountList" ofType="com.example.domain.Account"
  8. column="id"
  9. select="com.example.dao.AccountDao.findAllByUid"/>
  10. </resultMap>
  11. <select id="findAll" resultMap="userAccountMap">
  12. SELECT * FROM USER;
  13. </select>

主要的功能实现位于 中,对于账户列表的信息通过collection集合来映射,通过select指定集合中的每个元素如何查询,在本例中select的属性值为AccountDao.xml文件的namespace com.example.dao.AccountDao路径以及指定该映射文件下的findAllByUid方法,通过这个唯一标识指定集合中元素的查找方式。因为在这里需要用到根据用户ID查找账户,所以需要同时配置一下findAllByUid方法的实现。

4.配置collection中select属性所使用的方法 findAllByUid

AccountDao接口中添加

  1. /**
  2. * 根据用户ID查询账户信息
  3. * @return
  4. */
  5. List<Account> findAllByUid(Integer uid);

AccountDao.xml文件中配置

  1. <select id="findAllByUid" resultType="com.example.domain.Account">
  2. SELECT * FROM account WHERE uid = #{uid};
  3. </select>

5.在Mybatis的配置文件中开启全局延迟加载

  1. configuration>
  2. <settings>
  3. <!--开启全局的懒加载-->
  4. <setting name="lazyLoadingEnabled" value="true"/>
  5. <!--关闭立即加载,其实不用配置,默认为false-->
  6. <setting name="aggressiveLazyLoading" value="false"/>
  7. <!--开启Mybatis的sql执行相关信息打印-->
  8. <setting name="logImpl" value="STDOUT_LOGGING" />
  9. </settings>
  10. <typeAliases>
  11. <typeAlias type="com.example.domain.Account" alias="account"/>
  12. <typeAlias type="com.example.domain.User" alias="user"/>
  13. <package name="com.example.domain"/>
  14. </typeAliases>
  15. <environments default="test">
  16. <environment id="test">
  17. <!--配置事务-->
  18. <transactionManager type="jdbc"></transactionManager>
  19. <!--配置连接池-->
  20. <dataSource type="POOLED">
  21. <property name="driver" value="com.mysql.jdbc.Driver"/>
  22. <property name="url" value="jdbc:mysql://localhost:3306/test1"/>
  23. <property name="username" value="root"/>
  24. <property name="password" value="123456"/>
  25. </dataSource>
  26. </environment>
  27. </environments>
  28. <!--配置映射文件的路径-->
  29. <mappers>
  30. <mapper resource="com/example/dao/UserDao.xml"/>
  31. <mapper resource="com/example/dao/AccountDao.xml"/>
  32. </mappers>
  33. </configuration>

6.测试方法

  1. private InputStream in;
  2. private SqlSession session;
  3. private UserDao userDao;
  4. private AccountDao accountDao;
  5. private SqlSessionFactory factory;
  6. @Before
  7. public void init()throws Exception{
  8. //获取配置文件
  9. in = Resources.getResourceAsStream("SqlMapConfig.xml");
  10. //获取工厂
  11. factory = new SqlSessionFactoryBuilder().build(in);
  12. session = factory.openSession();
  13. userDao = session.getMapper(UserDao.class);
  14. accountDao = session.getMapper(AccountDao.class);
  15. }
  16. @After
  17. public void destory()throws Exception{
  18. session.commit();
  19. session.close();
  20. in.close();
  21. }
  22. @Test
  23. public void findAllTest(){
  24. List<User> userList = userDao.findAll();
  25. // for (User user: userList){
  26. // System.out.println("每个用户的信息");
  27. // System.out.println(user);
  28. // System.out.println(user.getAccountList());
  29. // }
  30. }

7.测试结果
(1)注释for循环,不使用数据,这时候不需要查询账户信息,通过第五步的sql语句控制台打印看出来,不使用数据的时候,就没有发起对账户的查询。
(2)通过for循环打印查询的数据,使用数据,这时候因为使用了数据所以将查询账户信息,我们可以通过控制台的sql语句打印发现用户和账户查询都进行了执行