Mybatis_延迟加载和缓存

Mybatis_延迟加载和缓存

问题

​ 在一对多的情况下,比如有一个用户,拥有100个账号:

  • 在查询用户时,要不要把关联的账户查出来?

    ​ 用户和账号是一对多的关系,一个用户对应很多个账号。如果查询用户的时候,同时关联查询它的账号,那么这些账号信息将占用很大的内存。所以实际情况下是,什么时候用到账号信息,什么时候查询对应的账号信息。

  • 在查询账户时,要不要把关联的用户信息查出来?

    ​ 账号和用户是一对一的关系,一个账号属于一个用户。查询账号的时候,同时关联它的用户信息,只会多一个对象所需的内存,所需内存空间较小。所以这种情况下账号和用户的信息是一起查询出来的。

延迟加载:在真正使用数据时才发起查询,不用的时候不查询,按需加载(懒加载)。多用于关联的对象为“多”时,即一对多,多对多。

立刻加载:管用不用,在调用方法的时候就立即执行,马上发起查询。多用户关联的对象为“一”时,即多对一,一对一。

延时加载

一对一的延迟加载

用户实体和账号实体

public class User implements Serializable {
  private Integer id;
  private String username;
  private String sex;
  private String address;
  private Date birthday;
  private List<Account> accounts;
  /* getter、setter and toString */
}
public class Account implements Serializable {
  private Integer id;
  private Integer uid;
  private Double money;
  
  private User user;
  /* getter、setter and toString */
}

账号对应的配置IAccountDao.xml

<resultMap id="accountUserMap" type="com.xijianlv.domain.Account">
  <!-- 封装account表的内容 -->
  <id property="id" column="id"></id>
  <result property="uid" column="uid"></result>
  <result property="money" column="money"></result>
  <!-- 一对一的关系映射,配置封装user的内容 -->
  <!-- select属性指定的内容:查询用户的唯一标识,即在IuserDao.xml中可以根据uid查询用户信息的配置 -->
  <!-- column属性指定的内容:在IuserDao.xml中根据uid查询用户信息的时候需要的参数 -->
  <association property="user" column="uid" javaType="com.xijianlv.domain.User"
               select="com.xijianlv.domain.User.findById">
  </association>
</resultMap>
<!-- 配置查询所有 -->
<select id="findAll" resultMap="accountUserMap">
  select * from account
</select>

在全局配置中添加如下配置开启延迟加载

<settings>
  <setting name="lazyLoadingEnabled" value="true"/>
  <!-- aggressiveLazyLoading属性在3.4.1之后的版本默认为false -->
  <setting name="aggressiveLazyLoading" value="false"/>
</settings>

测试方法及结果

@Test
public void testFindAll() {
  List<Account> accounts = iAccountDao.findAll();
  for (Account account : accounts) {
    System.out.println(account.toString());
  }
}

在这里插入图片描述

​ 从log可以看出,上述结果中执行了3条sql语句。由于测试方法的在输出account信息的时候调用了toString方法,这个方法里面有用到user的信息,所以后续两个sql在用到了user信息的情况下去查询了user的信息。

修改测试方法如下

@Test
public void testFindAll() {
  List<Account> accounts = iAccountDao.findAll();
  for (Account account : accounts) {
    System.out.println("Account(id=" + account.getId() +
                       ", uid=" + account.getUid() + ", money=" + account.getMoney() + ")");
  }
}

在这里插入图片描述

从log看出,只执行了一条sql语句来加载账户信息,并没有加载对应的用户信息。

一对多的延迟加载

修改IUserDao.xml配置文件如下

<!-- User的resultMap -->
<resultMap id="userAccountMap" type="com.xijianlv.domain.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>
  <!-- User中account集合的映射 -->
  <collection property="accounts" ofType="com.xijianlv.domain.Account"
              select="com.xijianlv.dao.IAccountDao.findAccountByUid" column="id">
  </collection>
</resultMap>
<!-- 配置查询所有 -->
<select id="findAll" resultMap="userAccountMap">
  select * from user
</select>

AccountDao接口中新增 根据用户id查询账户信息 的功能,对应的IAccountDao.xml新增如下配置:

<!-- 根据用户id查询账户信息 -->
<select id="findAccountByUid" resultType="com.xijianlv.domain.Account">
  select * from account where uid=#{uid}
</select>

测试方法及结果

@Test
public void testFindAll() {
  List<User> users = userDao.findAll();
  for (User user : users) {
    System.out.println("User{" +
                       "id=" + user.getId() +
                       ", username='" + user.getUsername() + '\'' + 
                       ", sex='" + user.getSex() + '\'' + 
                       ", address='" + user.getAddress() + '\'' + 
                       ", birthday=" + user.getBirthday() + '}');
  }
}

在这里插入图片描述

Mybatis中的缓存

​ Mybatis提供了一级缓存和二级缓存。

  • 一级缓存:它是 SqlSession 级别的缓存,SqlSession 类的实例对象中提供了一个 HashMap 的结构,可以用于存储缓存数据,当我们再次查询同一数据的时候,MyBatis 会先去 SqlSession 中查询,有的话,就直接调用
  • 二级缓存:是Mapper级别的缓存,也就是说,如果多个 SqlSession 类的实例,去操作同一个Mapper配置文件中的SQL,这些实例对象可以共用二级缓存

相关概念:

  • SqlSession:代表和数据库的一次会话,向用户提供了操作数据库的方法。
  • MappedStatement: 代表要发往数据库执行的指令,可以理解为是Sql的抽象表示。
  • Executor: 具体用来和数据库交互的执行器,接受MappedStatement作为参数。
  • 映射文件: 是Mybatis编写Sql的地方,通常来说每一张单表都会对应着一个映射文件,在该文件中会定义Sql语句入参和出参的形式。
  • 映射接口: 在接口中定义要进行的查询方法,具体的Sql写在映射文件中。

一级缓存

​ 每个SqlSession中都有一个Executor,每个Executor中有一个LocalCache。当用户发起查询时,MyBatis根据当前执行的语句生成MappedStatement,在Local Cache进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入Local Cache,最后返回结果给用户。

​ 但是,如果在下一次操作中,执行力commit操作,即执行力增删改操作,一级缓存中的内容会被清空,保证缓存中的数据的有效性,避免脏读。

在这里插入图片描述

​ 从BaseExecutor中得知,localCache的类型是PerpetualCache。在查看PerpetualCache得知,Mybatis的一级缓存是一个Map。

在这里插入图片描述

一级缓存实现

​ 在Mybatis中一级缓存有两个选项,SESSION或者STATEMENT,默认是SESSION级别,即在一个MyBatis会话中执行的所有语句,都会共享这一个缓存。一种是STATEMENT级别,可以理解为缓存只对当前执行的这一个Statement有效。

<!--默认值为SESSION,所以此配置可不添加-->
<setting name="localCacheScope" value="SESSION"/>

实体类

public class User implements Serializable {
  private Integer id;
  private String username;
  private String sex;
  private String address;
  private Date birthday;
  /* getter、setter and toString */
}

对应的dao接口的配置IUserDao.xml

<select id="findById" parameterType="Integer" resultType="com.xijianlv.domain.User">
  select * from user where id = #{uid}
</select>

测试方法和结果

@Test
public void testFirstLevelCache() {
  User user1 = userDao.findById(46);
  System.out.println(user1);
  User user2 = userDao.findById(46);
  System.out.println(user2);
  System.out.println(user1 == user2);
}

在这里插入图片描述

​ 从日志可以看出,两次查询得到的是同一个对象,而且第二次调用findById()方法,并没有执行sql的过程。

​ 如果在第一次findById()方法执行完之后,关闭sqlseession对象,从工厂重新获取一个,再次调用findById()方法,修改代码和结果如下:

@Test
public void testFirstLevelCache() {
  User user1 = userDao.findById(46);
  System.out.println(user1);
  sqlSession.close();
  //关闭sqlSession之后,从sqlSessionFactory重新获取sqlSession
  sqlSession = sqlSessionFactory.openSession(true);
  userDao = sqlSession.getMapper(IUserDao.class);
  User user2 = userDao.findById(46);
  System.out.println(user2);
  System.out.println(user1 == user2);
}

在这里插入图片描述

​ 从日志可以看出,两次查询得到的不是同一个对象,第一次获取完user1之后,关闭了原有的sqlSession,同时也意味着原有的Executor中缓存数据Map被释放掉了。再次获取到sqlSession,同时也重新在Executor中新建了一个Map来缓存数据。

​ sqlSession对象有一个clearCache()方法来清空缓存,修改测试方法如下:

    @Test
    public void testFirstLevelCache() {
        User user1 = userDao.findById(46);
        System.out.println(user1);
        sqlSession.clearCache();//清空缓存
        User user2 = userDao.findById(46);
        System.out.println(user2);
        System.out.println(user1 == user2);
    }

在这里插入图片描述

​ 从日志可以看出,两次查询得到的也不是同一个对象,不过和关闭sqlSession不通的是,两次查询使用的是同一个BaseExecutor,也就是说用的是同一个Map对象。

一级缓存的清空

​ 从上面的例子中,验证了mybatis的一级缓存。但是为了保证缓存中的数据是有效的,mybatis会在增删改之后清空缓存。

​ 新增一个更新方法,新增对应的配置

<update id="updateUser" parameterType="com.xijianlv.domain.User">
update user set username = #{username} where id = #{id}
</update>

测试和结果

@Test
public void testClearCache(){
  User user1 = userDao.findById(46);
  System.out.println(user1);
  user1.setUsername("Clear Cache test");
  userDao.updateUser(user1);
  User user2 = userDao.findById(46);
  System.out.println(user2);
  System.out.println(user1 == user2);
}

在这里插入图片描述

​ 从日志可以看出,在更新完操作之后从新执行sql获取了user信息,两次获取到的不是同一个对象。

在这里插入图片描述

​ 查看DefaultSqlSession.java源码,可以看出,insert、update和delete方法最终到手调用该类的update(String statement, Object parameter)方法。这个方法内部调用的为BaseExecutor类的update方法。如下图,在BaseExecutor类的update方法中进行了缓存的清空。

在这里插入图片描述

二级缓存

​ 一级缓存中,其最大的共享范围就是一个SqlSession内部,如果多个SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用CachingExecutor装饰Executor,进入一级缓存的查询流程前,先在CachingExecutor进行二级缓存的查询。

在这里插入图片描述

​ 二级缓存开启后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。

二级缓存的的配置
  1. 让Mybatis支持二级缓存。在SqlMapConfig.xml添加配置:

    <setting name="cacheEnabled" value="true"/>默认值为true,所以不配置也行。

  2. 让当前的映射文件支持二级缓存。在IUserDao.xml中添加配置:

    <cache/>
    
  3. 让当前的查询操作支持二级缓存。在对应的查询操作中添加useCache属性:

    <select id="findById" parameterType="Integer" resultType="com.xijianlv.domain.User" useCache="true">
      select * from user where id = #{uid}
    </select>
    

测试和结果

@Test
public void testSecondLevelCache() {
  sqlSession = sqlSessionFactory.openSession();
  IUserDao iUserDao1 = sqlSession.getMapper(IUserDao.class);
  User user1 = iUserDao1.findById(41);
  System.out.println(user1);
  sqlSession.close();//一级缓存消失
  sqlSession = sqlSessionFactory.openSession();
  IUserDao iUserDao2 = sqlSession.getMapper(IUserDao.class);
  User user2 = iUserDao2.findById(41);
  System.out.println(user2);
  System.out.println(user1 == user2);
}

在这里插入图片描述

​ 从日志中看到,只执行了一次sql的查询,说明二级缓存在不同的sqlSession间可共享。

​ 但是有所不同的是,获取的两个user对象却不是同一个,这和一级缓存的结果不一样。从这里即可得知,一级缓存存储的是对象,二级缓存储存的是数据。当访问二级缓存的时候,现将数据封装称对象,再返回给请求者。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值