Mybatis的延迟加载和缓存

文章目录

      • 1. 延迟加载策略
        • 1.1 概述
        • 1.2 实现延迟加载
          • 1.2.1 使用association实现
          • 1.2.2 使用collection实现
      • 2. 缓存
        • 2.1 一级缓存
        • 2.2 二级缓存
      • 3. 参考


1. 延迟加载策略

1.1 概述

在数据库的关联查询中,通常为了性能的考虑和实际的需求,并不需要在记载某类信息的时候同时加载关联的信息,此时就需要使用延迟加载策略

延迟加载是指在需要用到数据时才进行加载,不需要数据是就不进行加载操作,也称为懒加载。使用延迟加载的好处在于:从单表查询出发,需要时再从关联表去关联查询,大大的提高了数据库性能,因为单表查询的速度更快。缺点在于:因为只有当需要数据时才进行数据库查询。因此,在大批量数据查询时,查询功能耗时也较长,可能会造成用户等待时间变长,影响用户体验。

1.2 实现延迟加载

在关联查询中使用association和collection来进行关联查询结果之间的映射,而它们两个是具备延迟加载的功能的,因此,下面看一下如何实现延迟加载。

1.2.1 使用association实现

在之前的例子中我们在查询账户信息的时,结果会同时输出关联的用户信息。持久层接口定义为:

public interface IAccountDao {
    List<Account> findAll();
}

public interface IUserDao {
    List<User> findAll();

    User findById(Integer userId);
}

对应的xml文件配置:



<mapper namespace="dao.IAccountDao">
    <resultMap id="accountUserMap" type="account">
        <id property="id" column="id">id>
        <result property="uid" column="uid">result>
        <result property="money" column="money">result>

        <association property="user" column="uid" javaType="user" select="dao.IUserDao.findById">association>
    resultMap>
    <select id="findAll" resultMap="accountUserMap">
        select * from account
    select>
mapper>
<select id="findById" parameterType="INT" resultType="user">
	select * from user where id = #{uid}
select>

编写测试方法并执行单元测试:

    @Test
    public void testFindAll(){
        List<Account> accounts = accountDao.findAll();
    }

此时的log输出为:

- Opening JDBC Connection
- Created connection 1946645411.
- ==>  Preparing: select * from account
- ==> Parameters:
- ====>  Preparing: select * from user where id = ?
- ====> Parameters: 42(Integer)
- <====      Total: 1
- ====>  Preparing: select * from user where id = ?
- ====> Parameters: 43(Integer)
- <====      Total: 1
- <==      Total: 3
- Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@740773a3]
- Returned connection 1946645411 to pool.

从log中中可以看出,此时执行的SQL语句是select * from user where id = ?,它并没有实现懒加载。

如果要开启懒加载,需要在SqlMapConfig.xml中配置全局的懒加载机制。具体的配置方式如下:

    <settings>
        
        <setting name="lazyLoadingEnabled" value="true"/>
        <setting name="aggressiveLazyLoading" value="false">setting>
    settings>

这里用到了settings和setting标签,并且在setting标签内设置lazyLoadingEnabled和aggressiveLazyLoading这两个和懒加载相关的参数。配置完成后再次执行测试方法,此时的log输出为:

- Opening JDBC Connection
- Created connection 1706292388. - ==>  Preparing: select * from account
- ==> Parameters:
- <==      Total: 3
- Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@65b3f4a4]
- Returned connection 1706292388 to pool.

此时的SQL语句是select * from account,它并没有同时查询账户对应的用户信息,从而实现了懒加载。

1.2.2 使用collection实现

同样我们也可以在一对多关系配置的结点中配置延迟加载策略。 结点中也有select属性,column属性。例如,现在完成加载用户对象时,我们希望查询该用户所拥有的账户信息。

编写持久层接口:

public interface IAccountDao {
    List<Account> findAll();

    List<Account> findAccountByUid(Integer uid);
}

public interface IUserDao {
    List<User> findAll();

    User findById(Integer userId);
}

编写对应的xml文件,配置信息:

    <select id="findAccountByUid" resultType="account">
        select * from account where uid = #{uid}
    select>
<mapper namespace="dao.IUserDao">
    <resultMap id="userAccountMap" type="user">
        <id property="id" column="id">id>
        <result property="username" column="username">result>
        <result property="address" column="address">result>
        <result property="sex" column="sex">result>
        <result property="birthday" column="birthday">result>

        <collection property="accounts" ofType="account" select="dao.IAccountDao.findAccountByUid" column="id">
        collection>
        
    resultMap>
    <select id="findAll" resultMap="userAccountMap">
        select * from user
    select>
    <select id="findById" parameterType="INT" resultType="user">
        select * from user where id = #{uid}
    select>
mapper>

标签: 主要用于加载关联的集合对象

  • select属性: 用于指定查询account列表的sql语句,所以填写的是该sql映射的id
  • column属性: 用于指定select属性的sql语句的参数来源,上面的参数来自于user的id列,所以就写成id这一个字段名了

编写测试方法并执行单元测试:

	@Test
    public void testFindAll(){
        List<User> users = userDao.findAll();
    }

此时的log信息为:

- Opening JDBC Connection
- Created connection 270056930.
- Preparing: select * from user 
- ==> Parameters: 
- <==      Total: 4
- Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@1018bde2]
- Returned connection 270056930 to pool.

此时执行的SQL语句为select * from user,并没有同时查询账户信息,实现了懒加载。


2. 缓存

像大多数的持久化框架一样,Mybatis也提供了缓存策略,将一些经常查询,并且不经常改变的,以及数据的正确对最后的结果影响不大的数据,放置在一个缓存容器中,当用户再次查询这些数据的时候,就不必再去数据库中查询,直接在缓存中提取就可以了。Mybatis中缓存分为一级缓存、二级缓存。
Mybatis的延迟加载和缓存_第1张图片

2.1 一级缓存

Mybatis的延迟加载和缓存_第2张图片

一级缓存是SqlSession级别的缓存,只要SqlSession没有flush或close,它就存在。为了验证一级缓存的存在,我们在程序中测试findById

public interface IUserDao {
    User findById(Integer userId);
}
 <select id="findById" parameterType="INT" resultType="user">
        select * from user where id = #{uid}
  select>
    @Test
    public void testFindById(){
        User user = userDao.findById(41);
        System.out.println(user);

        User user2 = userDao.findById(41);
        System.out.println(user2);

        System.out.println(user == user2);
    }
}

执行单元测试,输出为:

User{id=41, username='Forlogen', address='Beijing', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018}
User{id=41, username='Forlogen', address='Beijing', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018}
true

可以看到执行了两次userDao.findById(41);得到的是相同的结果。因为,程序在第二次执行findById()时直接从一级缓存中取数据,而不是再次执行SQL语句。

如果在测试方法中,第一次执行完findById()后就关闭SqlSession对象,或者调用sqlSession.clearCache()清空缓存,第二次执行的结果和第一次的结果就不同了。

使用缓存不得不考虑的一个问题就是缓存中数据和数据库中数据的同步问题。这里一级缓存是SqlSession范围的缓存,当调用SqlSession的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。例如
Mybatis的延迟加载和缓存_第3张图片

  • 第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。 得到用户信息,将用户信息存储到一级缓存中
  • 如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
  • 第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息

例如在上面的例子中执行两次findById(),但是两次之间添加一个更新操作:

public interface IUserDao {
    void updateUser(User user);
}
 <update id="updateUser" parameterType="user">
        update user set username=#{username},address=#{address} where id=#{id}
    update>
    @Test
    public void testClearlCache(){
        //1.根据id查询用户
        User user1 = userDao.findById(41);
        System.out.println(user1);

        //2.更新用户信息
        user1.setUsername("update user clear cache");
        user1.setAddress("北京市海淀区");
        userDao.updateUser(user1);

        //3.再次查询id为41的用户
        User user2 = userDao.findById(41);
        System.out.println(user2);
        System.out.println(user1 == user2); // false
    }

此时的输出为:

User{id=41, username='Forlogen', address='Beijing', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018}
User{id=41, username='update user clear cache', address='北京市海淀区', sex='男', birthday=Wed Feb 28 01:47:08 CST 2018}
false

从输出中可知,更新操作成功,而且一级缓存在更新操作后被清空。因此,两次findById()的结果是不等的。

总结:

  • 在同一个 SqlSession 中, Mybatis 会把执行的方法和参数通过算法生成缓存的键值, 将键值和结果存放在一个 Map 中, 如果后续的键值一样, 则直接从 Map 中获取数据;
  • 不同的 SqlSession 之间的缓存是相互隔离的
  • 用一个 SqlSession, 可以通过配置使得在查询前清空缓存
  • 任何的 UPDATE, INSERT, DELETE 语句都会清空缓存

2.2 二级缓存

Mybatis的延迟加载和缓存_第4张图片

二级缓存是mapper映射级别的缓存,它存在于 SqlSessionFactory 生命周期中。多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

二级缓存的开启与关闭:

  • 在SqlMapConfig.xml文件开启二级缓存

    <settings>
            <setting name="cacheEnabled" value="true"/>
        settings>
    

    cacheEnabled取值为true,表示开启二级缓存,默认为true

  • 配置相关的Mapper映射文件

    <mapper namespace="dao.IUserDao">
        
        <cache/>
    mapper>
    

    标签表示当前这个mapper映射将使用二级缓存,区分的标准就看mapper的namespace值。其中中可以出现任意多个property子元素,同时也提供了一些可选属性,如:

    • type: 用于指定缓存的实现类型, 默认是PERPETUAL, 对应的是 mybatis 本身的缓存实现类 org.apache.ibatis.cache.impl.PerpetualCache

    • eviction:对应的是回收策略, 默认为 LRU,即最近最少使用, 移除最长时间不被使用的对象;此外还有:

      • FIFO: 先进先出, 按对象进入缓存的顺序来移除对象
      • SOFT: 软引用, 移除基于垃圾回收器状态和软引用规则的对象
      • WEAK: 弱引用, 移除基于垃圾回收器状态和弱引用规则的对象
    • flushInterval:对应刷新间隔, 单位毫秒, 默认值不设置, 即没有刷新间隔, 缓存仅仅在刷新语句时刷新。如果设定了之后, 到了对应时间会过期, 再次查询需要从数据库中取数据

    • size:`对应为引用的数量,即最多的缓存对象数据, 默认为 1024

    • readOnly:只读属性, 默认为 false,可选值有:

      • false: 可读写, 在创建对象时, 会通过反序列化得到缓存对象的拷贝。 因此在速度上会相对慢一点, 但重在安全。

      • true: 只读, 只读的缓存会给所有调用者返回缓存对象的相同实例。 因此性能很好, 但如果修改了对象, 有可能会导致程序出问题。

    • blocking:阻塞, 默认值为 false。 当指定为 true 时将采用 BlockingCache 进行封装。使用 BlockingCache 会在查询缓存时锁住对应的 Key,如果缓存命中了则会释放对应的锁,否则会在查询数据库以后再释放锁,这样可以阻止并发情况下多个线程同时查询数据

  • 配置statement上面的useCache属性

    <select id="findById" parameterType="INT" resultType="user" useCache="true">
            select * from user where id = #{uid}
        select>
    

    将UserDao.xml映射文件中的