vlambda博客
学习文章列表

MyBatis08:MyBatis加载策略及缓存

  点击上方

 蓝字 关注我吧

预计时间
MyBatis08:MyBatis加载策略及缓存

     阅读本文大概需要12分钟

MyBatis加载策略


什么是延迟加载?

问题

前面的文章中,我们了解了Mybatis中一对一,一对多,多对多关系的配置及实现,可以实现对象的关联查询。实际开发过程中很多时候我们并不需要总是在加载用户信息时就一定要加载他的订单信息。此时就是我们所说的延迟加载。


举个栗子

MyBatis08:MyBatis加载策略及缓存


延迟加载

就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载


注意:延迟加载是基于嵌套查询来实现的


* 优点:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。


* 缺点:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗时 间,所以可能造成用户等待时间变长,造成用户体验下降。


应用场景

* 在多表中:

一对多,多对多:通常情况下采用延迟加载 

一对一(多对一):通常情况下采用立即加载


实现

1. 局部延迟加载

associationcollection标签中都有一个fetchType属性,通过修改它的值,可以修改局部的加载策略。

UserMapper.xml

<!-- 开启一对多 延迟加载 -->

<!--一对多嵌套查询:查询所有的用户,同时还要查询出用户所关联的订单信息-->
<resultMap id="userOrderMap" type="com.zwt.domain.User">
    <id property="id" column="id"/>
    <result property="username" column="username"></result>
    <result property="birthday" column="birthday"></result>
    <result property="sex" column="sex"></result>
    <result property="address" column="address"></result>

    <!--
       fetchType="lazy" : 延迟加载策略
       fetchType="eager": 立即加载策略
 一对多:这里设置为局部延迟加载    -->
    <collection property="ordersList" ofType="com.zwt.domain.Orders"
                column="id" select="com.zwt.mapper.OrderMapper.findByUid" 
                fetchType="lazy">

    </collection>
</resultMap>

<select id="findAllWithOrder2" resultMap="userOrderMap">
        SELECT * FROM USER
</select>


2. 设置触发延迟加载的方法

大家在配置了延迟加载策略后,发现即使没有调用关联对象的任何方法,但是在你调用当前对象的equalsclonehashCodetoString方法时也会触发关联对象的查询。


我们可以在核心配置文件中使用 lazyLoadTriggerMethods 配置项覆盖掉上面四个方法。这样,即使调用该方法,也不会触发关联对象的查询了。

sqlMapConfig.xml

<settings> 
      <!--所有方法都会延迟加载--> 
      <setting name="lazyLoadTriggerMethods" value="toString()"/> 
</settings>


注意:settings标签是设置在properties之后的。

MyBatis08:MyBatis加载策略及缓存


3. 全局延迟加载

Mybatis的核心配置文件中可以使用setting标签修改全局的加载策略。

sqlMapConfig.xml

<settings> 
      <!--开启全局延迟加载功能--> 
      <setting name="lazyLoadingEnabled" value="true"/> 
</settings>


注意

局部的加载策略优先级高于全局的加载策略。

<!-- 关闭一对一 延迟加载 -->
<resultMap id="orderMap2" type="com.lagou.domain.Orders">
    <id property="id" column="id"/>
    <result property="ordertime" column="ordertime"/>
    <result property="total" column="total"/>
    <result property="uid" column="uid"/>
        <!--
            fetchType="lazy" : 延迟加载策略
            fetchType="eager": 立即加载策略
            一对一:这里设置为局部立即加载,优先级高于全局的延迟加载
        -->

     <association property="user" javaType="com.lagou.domain.User"
                  select="com.lagou.mapper.UserMapper.findById" 
                 column="uid" fetchType="eager"/>

    </association>
</resultMap>

<!--一对一嵌套查询-->
<select id="findAllWithUser2" resultMap="orderMap2">
     SELECT * FROM orders
</select>



MyBatis缓存



为什么使用缓存?

当用户频繁查询某些固定的数据时,第一次将这些数据从数据库中查询出来,保存在缓存中。

当用户再次查询这些数据时,不用再通过数据库查询,而是去缓存里面查询。

减少网络连接数据库查询带来的损耗,从而提高我们的查询效率,减少高并发访问带来的系统性能问题。

一句话概括:经常查询一些不经常发生变化的数据,使用缓存来提高查询效率。


像大多数的持久化框架一样,Mybatis也提供了缓存策略,通过缓存策略来减少数据库的查询次数,从而提高性能。 Mybatis中缓存分为一级缓存二级缓存


一级缓存

1. 什么是一级缓存?

一级缓存是SqlSession级别的缓存,是默认开启

所以在参数SQL完全一样的情况下,我们使用同一个SqlSession对象调用一个Mapper方法,往往只执行一次SQL,因为使用SelSession第一次查询后,MyBatis会将其放在缓存中,以后再查询的时候,如果没有声明需要刷新,并且缓存没有超时的情况下,SqlSession都会取出当前缓存的数据,而不会再次发送SQL到数据库。


2. 

    /*
        验证mybatis中的一级缓存
     */

    @Test
    public void testOneCache() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        // 根据id查询用户信息
        // 第一次查询,查询的数据库
        User user1 = userMapper.findById(1);
        System.out.println(user1);

        // 第二次查询,查询的是一级缓存
        User user2 = userMapper.findById(1);
        System.out.println(user2);

        sqlSession.close();
    }


我们可以发现,虽然在上面的代码中我们查询了两次,但最后只执行了一次数据库操作,这就是Mybatis提供给我们的一级缓存在起作用了。因为一级缓存的存在,导致第二次查询id1的记录时,并没有发出sql语句从数据库中查询数据,而是从一级缓存中查询。


3. 分析

一级缓存是SqlSession范围的缓存,执行SqlSessionC(增加)U(更新)D(删除)操作,或者调用clearCache()commit()close()方法,都会清空缓存

MyBatis08:MyBatis加载策略及缓存

MyBatis08:MyBatis加载策略及缓存

4. 清除

方式1: 在代码中进行手动清除

    /*
        验证mybatis中的一级缓存
     */

    @Test
    public void testOneCache() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        // 根据id查询用户信息
        // 第一次查询,查询的数据库
        User user1 = userMapper.findById(1);
        System.out.println(user1);

        // clearCache: 手动清空缓存
        sqlSession.clearCache();

        // 第二次查询,查询的是一级缓存
        User user2 = userMapper.findById(1);
        System.out.println(user2);

        sqlSession.close();
    }


方式2:在xml文件中设置自动清除

<!-- 每次查询时,都会清除缓存 --> 
<select flushCache="true"> </select>


二级缓存

1. 什么是二级缓存?

二级缓存是namspace级别(跨sqlSession)的缓存,是默认不开启

二级缓存的开启需要进行配置,配置方法很简单,只需要在映射XML文件配置 cache/> 就可以开启二级缓存了;
实现二级缓存的时候,MyBatis要求返回的POJO必须是可序列化的。也就是要求实现Serializable接口。

MyBatis08:MyBatis加载策略及缓存


2. 验证

a)配置核心配置文件

sqlMapConfig.xml

<settings>
    <!--
    因为cacheEnabled的取值默认就为true,所以这一步可以省略不配置。
    这里相当于一个开关:为true代表开启二级缓存,为false代表不开启二级缓存。
    -->

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


b)配置UserMapper.xml映射

<mapper namespace="com.zwt.mapper.UserMapper">
    <!--当前映射文件开启二级缓存-->
    <cache></cache>

    <!--
    <select>标签中设置useCache=”true”代表当前这个statement要使用二级缓存。
    如果不使用二级缓存可以设置为false
    注意:
        针对每次查询都需要最新的数据sql,
        要设置成useCache="false",禁用二级缓存。
    -->


    <!--根据id查询用户-->
    <select id="findById" resultType="com.zwt.domain.User" 
            parameterType="int" useCache="true">

        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>


c)修改User实体

//user实体类实现序列化
public class User  implements Serializable {

    private Integer id;
    private String username;
    private Date birthday;
    private String sex;
    private String address;

    // 表示多方关系:集合 : 代表了当前用户所具有的订单列表 collection
    private List<Orders> ordersList;

    // 表示多方关系:集合 : 代表了当前用户所具有的角色列表 collection
    private List<Role> roleList;

     //toString, get , set 方法略
}


d)测试结果

    /*
       验证mybatis中的二级缓存
    */

    @Test
    public void testTwoCache() throws IOException {
        InputStream resourceAsStream = Resources.getResourceAsStream("sqlMapConfig.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);

        SqlSession sqlSession1 = sqlSessionFactory.openSession();
        UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
        // 第一次查询
        User user = userMapper1.findById(1);
        System.out.println(user);

        // 注意:只有执行sqlSession.commit或者sqlSession.close,
        // 那么一级缓存中内容才会刷新到二级缓存
        sqlSession1.close();

        // 开启新的sqlSession,第二次查询
        SqlSession sqlSession2 = sqlSessionFactory.openSession();
        UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
        User user2 = userMapper2.findById(1);
        System.out.println(user2);

        sqlSession2.close();
    }


3分析

二级缓存是mapper映射级别的缓存,多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。

MyBatis08:MyBatis加载策略及缓存

1. 映射语句文件中的所有select语句将会被缓存。 

2. 映射语句文件中的所有insertupdatedelete语句会刷新缓存。


4. 注意问题(脏读)

mybatis的二级缓存因为是namespace级别,所以在进行多表查询时会产生脏读问题


举个栗子

1)假如需要 UserMapper.xml 查询一个用户及其关联的订单信息,第一次查询,将结果刷到二级缓存;

2)此时在OrderMapper.xml 中 执行 delete 这个用户的订单信息,此时,只是刷新了OrderMapper的二级缓存,但是UserMapper 的缓存并没有被刷新;

3)然后再发起第二次查询(参数相同),结果发现二级缓存中有值,直接使用缓存中的值返回。

看看,这不就出现脏读了嘛,真实数据库,他的订单已经减少啦!


tips:

1. mybatis的缓存,都不需要我们手动存储和获取数据。mybatis自动维护的。


2.mybatis开启了二级缓存后,那么查询顺序:二级缓存--》一级缓存--》数据库

3. 注意:mybatis的二级缓存会存在脏读问题,一般不要开启,业务中实在要用到缓存时,请使用第三方的缓存技术(如:redis)。




长按二维码

识别关注

共同成长