Mybatis缓存及延迟加载策略
一:引言
通过前面几篇的文章介绍了Mybatis的一对一、一对多、多对多关系的配置及实现,可是大家发现了吗?在执行关联查询的时候,直接会把当前查询的主表里包含的副表也查询后封装到对象里,其实在实际开发中,很多时候我们并不需要总是在查询主表数据时就一定要加载它们的副表数据,此时我们就可以使用延迟加载。还有就是在介绍完延迟加载策略后会说明一下Mybatis的缓存机制。
准备工作【搭建一个快速的小框架】
二:Mybatis延迟加载策略
1:什么是延迟加载
在用到副表数据的时候才进行数据加载,没使用到副表数据(就是Teacher对象里面包含List<Student> studnets属性为副表)时不会加载数据。所以说延迟加载也称懒加载
好处:先查询主表数据,后期用到副表数据时再从关联表中去查询数据,减少了一次性查询大量数据,大大提高了数据库的性能,因为查询单表要比关联查询多表速度快
坏处:因为只有在用到副表数据时才会进行数据库查询,如果有大批量数据查询时,因为查询工作也要消耗时间,所以可能会造成用户等待时间变成,体验下降
数据库中有四种表关系:一对多,多对一,一对一,多对多
一对多,多对多:通常情况下我们采用延迟加载
一对多,多对一:通常情况下我们采用立即加载(Mybatis没用多对一的概念,当一对一看)
2:问题阐述
假设在一对多表中。当我们有一个辅导员,他管理1000个学生,这就是典型的一对多,那我们在查询辅导员信息的时候要不要把关联的学生数据查出来呢?考虑性能就不需要查询,但是如果后期有需求,要打印辅导员对应的学生信息,这时候怎么办?在没设置延迟加载的查询时,只能再手动调用查询学生信息。
3:一对多的延迟加载
#####改造TeacherDao里面的teacher映射关系 <!--辅导员关系映射--> <resultMap id="teacherMapper" type="teacher"> <id column="tid" property="id"></id> <result column="tname" property="name"></result> <result column="tsex" property="sex"></result> <result column="tage" property="age"></result> <result column="tsalary" property="salary"></result> <result column="taddress" property="address"></result> <collection property="students" ofType="student" column="tid" select="cn.xw.dao.StudentDao.findByTid"></collection> </resultMap> <!-- collection:集合查询(一对多查询、多对多查询)标签 property:Teacher里的属性字段 private List<Student> students 封装到当前字段 ofType:当前封装数据的类型 column:这个是数据库字段,说白了就是使用当前表的什么字段值去查找匹配对应的表数据 也可以理解外键和外键对应字段查询 select:调用StudentDao的查询标签方法 -->
可是这个写完也没出现延迟加载的效果呀
要查询官方文档会发现要设置Settings标签属性
#####改造SqlMapConfig.xml文件 <!--在properties 下面标签配置settings 保证顺序不会错--> <!--配置settings标签属性--> <settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings> ######更改测试类的findByIdTeacher方法 //查询单个辅导员 @Test public void findByIdTeacher(){ sqlSession = factory.openSession(); TeacherDao mapper = sqlSession.getMapper(TeacherDao.class); Teacher teacher = mapper.findById(1); System.out.println("获取辅导员信息 不打印对应的从表(副表)数据"); System.out.println(teacher.getName()+" "+teacher.getSex()+" "+teacher.getAddress()); System.out.println("开始打印对应的从表数据学生"); for (Student student:teacher.getStudents()){ System.out.println("学生数据:"+student); return; } }
4:一对一延迟加载(建议不使用延迟加载)
其实一对一的延迟加载和一对多的延迟加载差不多,只是标签使用的不同,我假设一个学生都对应了一个家庭表信息,那一个家庭表信息对应一个学生,简单的一对一,接下来我来展示一下延迟加载,我先对代码进行一些改造。
##### 因为基本的查询单个学生的代码已经写好了,只是缺少一个测试 //根据id查询学生id @Test public void findByIdStudent(){ sqlSession = factory.openSession(); StudentDao mapper = sqlSession.getMapper(StudentDao.class); Student student = mapper.findById(1); System.out.println("打印学生的基本信息"); System.out.println(student.getName()+" "+student.getAge()+" "+student.getAddress()); System.out.println("打印家庭信息"); System.out.println(student.getFamily()); }
开始对一对一延迟加载的演示
##### 开始对StudentDao.xml文件的配置进行修改 <!--配置学生类映射关系--> <resultMap id="studentMapper" type="student"> <id column="sid" property="id"></id> <result column="sname" property="name"></result> <result column="ssex" property="sex"></result> <result column="sage" property="age"></result> <result column="scredit" property="credit"></result> <result column="smoney" property="money"></result> <result column="saddress" property="address"></result> <result column="senrol" property="enrol"></result> <association property="family" javaType="family" column="fid" select="cn.xw.dao.FamilyDao.findById"></association> </resultMap> <!-- association:关联查询(一对一查询、多对一查询)标签 property:Student里的属性字段 private Family family 封装到当前字段 ofType:当前封装数据的类型 column:这个是数据库字段,说白了就是使用当前表的什么字段值去查找匹配对应的表数据 也可以理解外键和外键对应字段查询 当前表的fid对应从表的fid select:调用FamilyDao的查询指定标签方法 -->
出现这个问题是因为我们没用加settings配置 和上面的一对多配置是一样的
<!--在properties 下面标签配置settings 保证顺序不会错--> <!--配置settings标签属性--> <settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>
5:小结
Mybatis的延迟加载策略是在多表查询的关联嵌套查询和集合嵌套查询的基础上展开的,总的来说一对多推荐延迟加载,但是一对一就不建议了,因为一对一后面的从表数据也就一条,使用前面的关联嵌套结果查询就可以了,就是说直接写多表连接查询然后直接映射结果,省去了来回查询的步骤
三:Mybatis缓存
1:什么是Mybatis缓存
缓存的英文名称叫cache,数据存在内存之中,用于零时存储,但是项目停止或者断电将失去缓存数据。使用缓存可以减少与数据库的交互次数,提高执行效率,但是经常发送改变的数据不推荐缓存,Mybatis也为我们提供了一级缓存和二级缓存
2:一级缓存
一级缓存是存在与SqlSession对象里面的,只要SqlSession没用使用flush或者close,它就会存在缓存
##### 我们针对测试类的findAllStudent方法进行修改 //查询全部学生 @Test public void findAllStudent() { //被初始化了一个SqlSession的一级缓存对象 sqlSession = factory.openSession(); //第一个代理对象 StudentDao mapper1 = sqlSession.getMapper(StudentDao.class); List<Student> students1 = mapper1.findAll(); System.out.println("打印第一个查询出来的hashcode"+students1.hashCode()); //第二个代理对象 StudentDao mapper2 = sqlSession.getMapper(StudentDao.class); List<Student> students2 = mapper2.findAll(); System.out.println("打印第二个查询出来的hashcode"+students2.hashCode()); System.out.println("比较2个对象是不是同一个"); System.out.println(students1==students2); }
那怎么样才可以让一级缓存消失呢?
清除一级缓存:
当SqlSession调用修改update、删除delete、添加insert、commint()、close()、clearCache()等会清除一级缓存
clearCache是清除缓存的方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
//查询全部学生 @Test public void findAllStudent() { //被初始化了一个SqlSession的一级缓存对象 sqlSession = factory.openSession(); //第一个代理对象 StudentDao mapper1 = sqlSession.getMapper(StudentDao.class); List<Student> students1 = mapper1.findAll(); System.out.println("打印第一个查询出来的hashcode"+students1.hashCode()); //清除缓存 sqlSession.clearCache(); //第二个代理对象 StudentDao mapper2 = sqlSession.getMapper(StudentDao.class); List<Student> students2 = mapper2.findAll(); System.out.println("打印第二个查询出来的hashcode"+students2.hashCode()); System.out.println("比较2个对象是不是同一个"); System.out.println(students1==students2); } /** * 注意 一级缓存SqlSession对象在当前的实例对象SqlSession对象内部起作用 * 什么意思呢?就是说通过factory工厂创建了多个SqlSession对象,它们之间的一级缓存 * 互不干扰,在SqlSessionA的一级缓存里面操作数据增删改及清除缓存都不会影响到 * SqlSessionB的一级缓存 */
所以总的来说,如果想要清除一级缓存之间使用clearCache方法或者commit方法即可,但是如果又想保留当前SqlSession缓存,又要进行增删改操作,那么避开的方法就是定义别的方法和重写使用工厂开辟一个SqlSession实例,然后在那边操作
3:二级缓存
二级缓存是存在与mapper映射级别的缓存(如:xxDao.xml),多个SqlSession去操作同一个Mapper映射的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的
首先要实现二级缓存是要手都配置属性和标签,否则不会出现二级缓存
注:首先要再SqlMapConfig.xml文件里配置stettings标签 <!--在properties 下面标签配置settings 保证顺序不会错--> <!--配置settings标签属性 cacheEnabled默认是true 可以不配置--> <settings> <setting name="cacheEnabled" value="true"/> </settings> ++++++++++++++++++++++++++++++++++++++++++ 在Teacher实体类序列化 实现Serializable接口 public class Teacher implements Serializable { ....... } ++++++++++++++++++++++++++++++++++++++++++ 在TeacherDao.xml文件中配置相应标签及属性 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="cn.xw.dao.TeacherDao"> <!--一定要配置此属性 代表这个Mapper开启二级缓存--> <cache></cache> <!--辅导员关系映射--> <resultMap id="teacherMapper" type="teacher"> <id column="tid" property="id"></id> <result column="tname" property="name"></result> <result column="tsex" property="sex"></result> <result column="tage" property="age"></result> <result column="tsalary" property="salary"></result> <result column="taddress" property="address"></result> </resultMap> <!--查询全部辅导员信息--><!--一定要设置useCade="true"--> <select id="findAll" resultMap="teacherMapper" useCache="true"> select * from teacher; </select> <!--查询单个辅导员信息 根据id--> <select id="findById" parameterType="Integer" resultMap="teacherMapper"> select * from teacher where tid=#{id}; </select> </mapper>
问题一:为什么要在指定的select语句标签里设置useCache=“true”?
因为在Dao.xml设置<cache/>标签代表这个mapper可以使用二级缓存了,但是这个Dao.xml里的哪些查询语句要有二级缓存呢?所以通过useCache属性来区别,因为我有的查询要进行二级缓存,有的重要数据,经常更新的数据就不能有二级缓存,而哪些增删改就不能二级缓存,也没意义,而且也没有useCache属性
问题二:为什么当前的mapper的数据有缓存后,而查询的hashcode和对象比较都不一样呢?
因为二级缓存和一级缓存不一样,一级缓存是缓存其Map结果,而Map里面包含查询的对象,如{ "key1" , new Student("小王","男",25)};而二级缓存结果也是Map,但是缓存的是散装数据,如{ "key1" , "小王" },{ "key2" , "小张" }