延迟加载
1、延迟加载:先从单表查询、需要时再从关联表去关联查询,大大提高 数据库性能,因为查询单表要比关联查询多张表速度要快。
2、如果查询角色并且关联查询动漫信息。如果先查询角色信息即可满足要求,当我们需要查询动漫信息时再查询动漫信息。把对动漫信息的按需去查询就是延迟加载。
3、resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),association、collection具备延迟加载功能。
4、使用association实现延迟加载
- 映射文件
<?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"> <!-- namespace命名空间,作用就是对sql进行分类化管理,理解sql隔离 注意:使用mapper代理方法开发,namespace有特殊重要的作用--> <mapper namespace="ecut.association.mapper.RoleMapperComic"> <resultMap type="Comic" id="ComicResultMap"> <id column="id" property="id"/> <result column="comic_name" property="comicName"/> <result column="remark" property="remark"/> </resultMap> <!-- 延迟加载的resultMap --> <resultMap type="Role" id="RoleLazyResultMap"> <!-- 配置映射的角色信息 --> <!-- id:指定查询列中的唯 一标识,角色信息的中的唯 一标识,如果有多个列组成唯一标识,配置多个id column:角色信息的唯 一标识 列 property:角色信息的唯 一标识 列所映射到Role中哪个属性 --> <id column="id" property="id"/> <result column="role_name" property="roleName"/> <result column="comic_id" property="comicId"/> <result column="note" property="note"/> <!-- 实现对动漫信息进行延迟加载 select:指定延迟加载需要执行的statement的id(是根据comic_id查询动漫信息的statement) 要使用映射文件中findComicById完成根据动漫id(comic_id)动漫信息的查询,如果findComicById不在本mapper中需要前边加namespace column:角色信息中关联动漫信息查询的列,是comic_id 关联查询的sql理解为: SELECT t_role.*, (SELECT comic_name FROM t_comic WHERE t_comic.id = t_role.id)comicName, (SELECT remark FROM t_comic WHERE t_comic.id = t_role.id)remark FROM t_comic --> <association property="comic" javaType="Comic" select="findComicById" column="comic_id"/> <!-- 实现对动漫信息进行延迟加载 --> </resultMap> <!-- 查询角色关联查询动漫信息 ,使用resultMap和延迟加载, 动漫信息需要延迟加载 --> <select id="findRoleComicsByLazyLoading" resultMap="RoleLazyResultMap"> SELECT * FROM t_role </select> <select id="findComicById" parameterType="java.lang.Integer" resultMap="ComicResultMap"> SELECT * FROM t_comic WHERE id = #{value} </select> </mapper>
需要定义两个mapper的方法对应的statement。
a、只查询角色信息
SELECT * FROM t_role
在查询角色的statement中使用association去延迟加载(执行)下边的satatement(关联查询动漫信息)
b、关联查询动漫信息
通过上边查询到的角色信息中comic_id去关联查询动漫信息,使用配置文件中的findComicById
上边先去执行findRoleComicsByLazyLoading,当需要去查询动漫的时候再去执行findComicById,通过resultMap的定义将延迟加载执行配置起来,
使用association中的select指定延迟加载去执行的statement的id。
-
接口类
package ecut.association.mapper; import java.util.List; import ecut.association.po.Comic; import ecut.association.po.Role; import ecut.association.po.RoleComic; public interface RoleMapperComic { //根据id查询动漫 public Comic findComicById(Integer id) throws Exception ; //查询角色信息,关联查询所属动漫信息,使用resultMap和延迟加载 public List<Role> findRoleComicsByLazyLoading() throws Exception ; }
- 配置文件
<!-- 全局配置参数,需要时再设置 --> <settings> <!-- 打开延迟加载 的开关 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 将积极加载改为消极加载即按需要加载 --> <setting name="aggressiveLazyLoading" value="false"/> </settings>
在配置文件中开启延迟查询(默认false),并将积极加载改为消极加载(默认true)
- 测试类
package ecut.association.test; import java.io.IOException; import java.io.InputStream; import java.util.List; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Before; import org.junit.Test; import ecut.association.mapper.RoleMapperComic; import ecut.association.po.Comic; import ecut.association.po.Role; import ecut.association.po.RoleComic; public class ComicMapperTest { private SqlSessionFactory factory; @Before public void init() throws IOException { String resource = "ecut/association/mybaits-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactoryBuilder builer =new SqlSessionFactoryBuilder(); factory = builer.build(inputStream); } @Test public void testFindRoleComicsByLazyLoading() throws Exception { SqlSession session = factory.openSession(); RoleMapperComic mapper = session.getMapper(RoleMapperComic.class); List<Role> roleComics = mapper.findRoleComicsByLazyLoading(); //执行getComic()去查询动漫信息,这里实现按需懒加载 System.out.println(roleComics); } }
执行上边mapper方法(findRoleComicsByLazyLoading),内部去调用映射文件中的findComicById只查询角色信息(单表)。
在程序中去遍历上一步骤查询出的List<Role>,当我们调用roleComics中的getComic方法时,开始进行延迟加载。
延迟加载,去调用映射文件中findComicById这个方法获取动漫信息。
- 运行结果
DEBUG [main] - ==> Preparing: SELECT * FROM t_role DEBUG [main] - ==> Parameters: DEBUG [main] - <== Total: 7 DEBUG [main] - ==> Preparing: SELECT * FROM t_comic WHERE id = ? DEBUG [main] - ==> Parameters: 1(Integer) DEBUG [main] - <== Total: 1 DEBUG [main] - ==> Preparing: SELECT * FROM t_comic WHERE id = ? DEBUG [main] - ==> Parameters: 2(Integer) DEBUG [main] - <== Total: 1 DEBUG [main] - ==> Preparing: SELECT * FROM t_comic WHERE id = ? DEBUG [main] - ==> Parameters: 3(Integer) DEBUG [main] - <== Total: 1 [ 角色信息:id =1,roleName =Saber, note = fate stay night, comicId = 1;动漫信息:id=1,comicName=null,remark=null, 角色信息:id =2,roleName =鸣人, note = 火影忍者, comicId = 2;动漫信息:id=2,comicName=null,remark=null, 角色信息:id =3,roleName =佐助, note = 火影忍者, comicId = 2;动漫信息:id=2,comicName=null,remark=null, 角色信息:id =4,roleName =赤丸, note = 火影忍者, comicId = 2;动漫信息:id=2,comicName=null,remark=null, 角色信息:id =5,roleName =兜, note = 火影忍者, comicId = 2;动漫信息:id=2,comicName=null,remark=null, 角色信息:id =6,roleName =鹿丸, note = 火影忍者, comicId = 2;动漫信息:id=2,comicName=null,remark=null, 角色信息:id =7,roleName =萨博, note = 海贼王, comicId = 3;动漫信息:id=3,comicName=null,remark=null]
先查询角色表然后根据comi_id去查询动漫表,查询了三次因为有三个不同的comic_id,相同的comic_id不会调用查询因为会存在缓存。
4、不使用mybatis提供的association及collection中的延迟加载功能,如何实现延迟加载??
- 定义两个mapper方法:查询角色列表、根据动漫id查询动漫信息
- 先去查询第一个mapper方法,获取角色信息列表
- 在程序中(service),按需去调用第二个mapper方法去查询动漫信息。
- 总之:使用延迟加载方法,先去查询简单的sql(最好单表,也可以关联查询),再去按需要加载关联查询的其它信息。
查询缓存
1、mybatis提供查询缓存,用于减轻数据压力,提高数据库性能。
2、mybaits提供一级缓存,和二级缓存。
- 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
- 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
3、作用:如果缓存中有数据就不用从数据库中获取,大大提高系统性能。
一级缓存
1、工作原理
- 第一次发起查询动漫id为1的动漫信息,先去找缓存中是否有id为1的动漫信息,如果没有,从数据库查询动漫信息。得到动漫信息,将动漫信息存储到一级缓存中。
- 如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
- 第二次发起查询动漫id为1的动漫信息,先去找缓存中是否有id为1的动漫信息,缓存中有,直接从缓存中获取动漫信息。
2、测试案例
mybatis默认支持一级缓存,不需要在配置文件去配置。
package ecut.association.test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;
import ecut.association.mapper.RoleMapperComic;
import ecut.association.po.Comic;
import ecut.association.po.Role;
import ecut.association.po.RoleComic;
public class RoleMapperComicTest {
private SqlSessionFactory factory;
@Before
public void init() throws IOException {
String resource = "ecut/association/mybaits-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactoryBuilder builer =new SqlSessionFactoryBuilder();
factory = builer.build(inputStream);
}
// 一级缓存测试
@Test
public void testCache1() throws Exception {
SqlSession session = factory.openSession();// 创建代理对象
RoleMapperComic mapper = session.getMapper(RoleMapperComic.class);
// 下边查询使用一个SqlSession
// 第一次发起请求,查询id为1的动漫
Comic comic1 = mapper.findComicById(1);
System.out.println("动漫信息:id="+comic1.getId()+",comicName="+comic1.getComicName()+",remark="+comic1.getRemark());
// 如果session去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
/*// 更新comic1的信息
comic1.setComicName("fate");
mapper.updateComic(comic1);
//执行commit操作去清空缓存
session.commit();*/
// 第二次发起请求,查询id为1的动漫
Comic comic2 = mapper.findComicById(1);
System.out.println("动漫信息:id="+comic2.getId()+",comicName="+comic2.getComicName()+",remark="+comic2.getRemark());
session.close();
}
}
第二次查询直接从缓存中获取没有查询数据库,若进行了update操作缓存中的数据被清空,第二次查询需要重新查询数据库
3、应用场景
正式开发,是将mybatis和spring进行整合开发,事务控制在service中。一个service方法中包括 很多mapper方法调用。
二级缓存
1、工作原理
- 首先开启mybatis的二级缓存。
- sqlSession1去查询动漫id为1的动漫信息,查询到动漫信息会将查询数据存储到二级缓存中。
- 如果SqlSession3去执行相同 mapper下sql,执行commit提交,清空该 mapper下的二级缓存区域的数据。
- sqlSession2去查询动漫id为1的动漫信息,去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
- 二级缓存与一级缓存区别,二级缓存的范围更大,多个sqlSession可以共享一个UserMapper的二级缓存区域。
- RoleMapperComic有一个二级缓存区域(按namespace分) ,其它mapper也有自己的二级缓存区域(按namespace分)。
- 每一个namespace的mapper都有一个二缓存区域,两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同 的二级缓存区域中。
2、测试案例
- 配置文件中开启二级缓存
<!-- 全局配置参数,需要时再设置 --> <settings> <!-- 打开延迟加载 的开关 --> <setting name="lazyLoadingEnabled" value="true"/> <!-- 将积极加载改为消极加载即按需要加载 --> <setting name="aggressiveLazyLoading" value="false"/> <!-- 开启二级缓存 --> <setting name="cacheEnabled" value="true"/> </settings>
mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓存。
- 映射文件中开启二级缓存
<cache />
在RoleMapperComic.xml中开启二缓存,RoleMapperComic.xml下的sql执行完成会存储到它的缓存区域(HashMap)。
- POJO类
package ecut.association.po; import java.io.Serializable; import java.util.List; public class Comic implements Serializable { private static final long serialVersionUID = -3674954993814032572L; private Integer id; private String comicName; private String remark; private List<Role> roles; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getComicName() { return comicName; } public void setComicName(String comicName) { this.comicName = comicName; } public String getRemark() { return remark; } public void setRemark(String remark) { this.remark = remark; } public List<Role> getRoles() { return roles; } public void setRoles(List<Role> roles) { this.roles = roles; } }
为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一样在内存。
- 测试类
package ecut.association.test; import java.io.IOException; import java.io.InputStream; import java.util.List; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Before; import org.junit.Test; import ecut.association.mapper.RoleMapperComic; import ecut.association.po.Comic; import ecut.association.po.Role; import ecut.association.po.RoleComic; public class RoleMapperComicTest { private SqlSessionFactory factory; @Before public void init() throws IOException { String resource = "ecut/association/mybaits-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactoryBuilder builer =new SqlSessionFactoryBuilder(); factory = builer.build(inputStream); } // 二级缓存测试 @Test public void testCache2() throws Exception { SqlSession session1 = factory.openSession(); SqlSession session2 = factory.openSession(); SqlSession session3 = factory.openSession(); // 创建代理对象 RoleMapperComic mapper1 = session1.getMapper(RoleMapperComic.class); // 第一次发起请求,查询id为1的动漫 Comic comic1 = mapper1.findComicById(1); System.out.println("动漫信息:id="+comic1.getId()+",comicName="+comic1.getComicName()+",remark="+comic1.getRemark()); //这里执行关闭操作,将session中的数据写到二级缓存区域 session1.close(); //使用session3执行commit()操作 RoleMapperComic mapper3 = session3.getMapper(RoleMapperComic.class); Comic comic = mapper3.findComicById(1); comic.setComicName("fate"); mapper3.updateComic(comic); //执行提交,清空RoleMapperComic下边的二级缓存 session3.commit(); session3.close(); RoleMapperComic mapper2 = session2.getMapper(RoleMapperComic.class); // 第二次发起请求,查询id为1的动漫 Comic comic2 = mapper2.findComicById(1); System.out.println("动漫信息:id="+comic2.getId()+",comicName="+comic2.getComicName()+",remark="+comic2.getRemark()); session2.close(); } }
即使session关闭也可以在二级缓存中获取数据,无需查询数据库。
3、useCache和刷新缓存的配置
- 在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。
<select id="findComicById" parameterType="java.lang.Integer" resultMap="ComicResultMap" useCache="false"> SELECT * FROM t_comic WHERE id = #{value} </select>
总结:针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存。
-
在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。设置statement配置中的flushCache="true" 属性,默认情况下为true即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
<update id="updateComic" parameterType="Comic" flushCache="true"> update t_comic set comic_name = #{comicName} where id =#{id} </update>
总结:一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。
4、二级缓存应用场景
- 对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。
- 实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。
5、二级缓存的局限性
- mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。
MyBaitis整合Ehcache
1、分布式缓存
不使用分布缓存,缓存的数据在各各服务单独存储,不方便系统 开发。所以要使用分布式缓存对缓存数据进行集中管理。
mybatis无法实现分布式缓存,需要和其它分布式缓存框架进行整合。
2、整合方法
- mybatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。
- mybatis和ehcache整合,mybatis和ehcache整合包中提供了一个cache接口的实现类。
- 接口完整接口名org.apache.ibatis.cache.Cache
3、整合步骤(参照官网配置http://www.mybatis.org/ehcache-cache/)
- 增加jar包,下载地址https://github.com/mybatis/ehcache-cache/releases/download/mybatis-ehcache-1.0.3/mybatis-ehcache-1.0.3.zip
- 整合ehcache
<!-- 开启本mapper的namespace下的二缓存 type:指定cache接口的实现类的类型,mybatis默认使用PerpetualCache 要和ehcache整合,需要配置type为ehcache实现cache接口的类型 --> <cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
配置mapper中cache中的type为ehcache对cache接口的实现类型。
- 加入ehcache配置文件
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE ehcache > <ehcache> <!-- 存储位置 --> <diskStore path="java.io.tmpdir"/> <!-- 默认的cache配置 --> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> </ehcache>
在classpath下配置ehcache.xml
- 遇到的问题:使用ehcache-3.5.2.jar包时会抛出java.lang.NoClassDefFoundError: net/sf/ehcache/Ehcache
的异常,是因为jar包添加错误,应该使用ehcache-core-2.10.3.jar
转载请于明显处标明出处