Mybatis学习(6)动态加载、一二级缓存

时间:2023-03-09 08:34:46
Mybatis学习(6)动态加载、一二级缓存

一、动态加载:

resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),association、collection具备延迟加载功能。

需求:

如果查询订单并且关联查询用户信息。如果先查询订单信息即可满足要求,当需要查询用户信息时再查询用户信息。把对用户信息的按需去查询就是延迟加载。

需要先说明下是按照这个sql的思路来实现延迟加载的:

mysql> select orders.*, (select user.username from user where orders.user_id = user.id) username from orders;
Mybatis学习(6)动态加载、一二级缓存
(对于这种查询,可以分成两部来理解,首先忽略整个select子查询,查出订单表中的数据,然后根据订单表的user_id执行子查询,对于一个orders的user_id,子查询只能返回一条数据,如果子查询返回多条数据则会出错,另外,每一条select子查询只能查询一个字段。)
mapper.xml:
<mapper namespace="com.cy.mapper.OrdersMapperCustom">
<!-- 延迟加载的resultMap -->
<resultMap type="com.cy.po.Orders" id="OrdersLazyLoadingUser">
<id column="id" property="id"></id>
<result column="user_id" property="userId"></result>
<result column="number" property="number"></result>
<result column="createtime" property="createtime"></result>
<result column="note" property="note"></result> <!-- 实现对用户信息进行延迟加载
select:指定延迟加载需要执行的statement的id(是根据user_id查询用户信息的statement)
要使用userMapper.xml中findUserById完成根据用户id(user_id)用户信息的查询,如果findUserById不在本mapper中需要前边加namespace
column:订单信息中关联用户信息查询的列,是user_id
关联查询的sql理解为:
SELECT orders.*,
(SELECT username FROM USER WHERE orders.user_id = user.id)username,
(SELECT sex FROM USER WHERE orders.user_id = user.id)sex
FROM orders
-->
<association property="user" javaType="com.cy.po.User" column="user_id" select="com.cy.mapper.UserMapper.findUserById"> </association>
</resultMap> <!-- 查询订单关联查询用户,用户信息需要延迟加载 -->
<select id="findOrdersLazyLoadingUser" resultMap="OrdersLazyLoadingUser">
select * from orders;
</select>
</mapper>

这边需要配置settings  延迟加载:

<settings>
<!-- 打开延迟加载 的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载即按需要加载 -->
<setting name="aggressiveLazyLoading" value="false"/
</settings>

mapper接口:

//查询订单关联查询用户,用户信息是延迟加载
public List<Orders> findOrdersLazyLoadingUser() throws Exception;

测试代码:

// 查询订单关联查询用户,用户信息使用延迟加载
@Test
public void FindOrdersLazyLoadingUser() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
OrdersMapperCustom ordersMapperCustom = sqlSession.getMapper(OrdersMapperCustom.class);
// 查询订单信息(单表)
List<Orders> orders = ordersMapperCustom.findOrdersLazyLoadingUser(); for(Orders order: orders){
// 执行getUser()去查询用户信息,这里实现按需加载
User user = order.getUser();
System.out.println(user);
}
sqlSession.close();
}

可以看到console输出的打印语句:

DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@20e6423f]
DEBUG [main] - ==> Preparing: select * from orders;
DEBUG [main] - ==> Parameters:
DEBUG [main] - <== Total: 3
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
------->> User [id=1, username=王五, sex=2, birthday=null, address=null]
------->> User [id=1, username=王五, sex=2, birthday=null, address=null]
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE id=?
DEBUG [main] - ==> Parameters: 10(Integer)
DEBUG [main] - <== Total: 1
------->> User [id=10, username=张三, sex=1, birthday=Thu Jul 10 00:00:00 CST 2014, address=北京市]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@20e6423f]

二、1级缓存、2级缓存:

mybatis提供查询缓存,用于减轻数据压力,提高数据库性能。

Mybatis学习(6)动态加载、一二级缓存

一级缓存是SqlSession级别的缓存。在操作数据库时需要构造 sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。

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

1.一级缓存:

工作原理:

Mybatis学习(6)动态加载、一二级缓存

第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。

如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。

第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。

测试代码:

一级缓存不需要在配置文件中配置,mybatis默认支持一级缓存;

 // 一级缓存测试
@Test
public void testCache1() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 第一次发起请求,查询id为1的用户
User user1 = userMapper.findUserById(1);
System.out.println(user1); // 如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
// 更新user1的信息
// user1.setUsername("测试用户22");
// userMapper.updateUser(user1);
// //执行commit操作去清空缓存
// sqlSession.commit(); // 第二次发起请求,查询id为1的用户
User user2 = userMapper.findUserById(1);
System.out.println(user2); sqlSession.close();
}

一级缓存测试

2.二级缓存:

原理:

Mybatis学习(6)动态加载、一二级缓存

mybatis全局配置settings中默认开始二级缓存;

sqlSession1去查询用户id为1的用户信息,查询到用户信息会将查询数据存储到二级缓存中。

如果SqlSession3去执行相同 mapper下sql,执行commit提交,清空该 mapper下的二级缓存区域的数据。

sqlSession2去查询用户id为1的用户信息,去缓存中找是否存在数据,如果存在直接从缓存中取出数据。

二级缓存与一级缓存区别:

二级缓存的范围更大,多个sqlSession可以共享一个UserMapper的二级缓存区域。

二级缓存区域(按namespace分)。两个mapper的namespace如果相同,这两个mapper执行sql查询到数据将存在相同 的二级缓存区域中。

配置:

1)开启mybatis的二级缓存:

<settings>
<!-- 打开延迟加载 的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 将积极加载改为消极加载即按需要加载 -->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- 开启二级缓存 默认即是开启的 -->
<setting name="cacheEnabled" value="true"/>
</settings>

2)在mapper.xml中开启二级缓存:

<mapper namespace="com.cy.mapper.UserMapper">

    <!-- 开启本mapper的namespace下的二级缓存 -->
<cache/> ....
</mapper>

3) pojo类实现序列化接口:

为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定在内存。

public class User implements Serializable{
//属性名和数据库表的字段对应
private int id;
private String username;// 用户姓名
private String sex;// 性别
private Date birthday;// 生日
private String address;// 地址
.....
}

测试代码:

1.先单纯的测试跨sqlSession级别的二级缓存,查看发出的sql语句:

     // 二级缓存测试
@Test
public void testCache2() throws Exception {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次发起请求,查询id为1的用户
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
//这里执行关闭操作,将sqlsession中的数据写到二级缓存区域
sqlSession1.close(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次发起请求,查询id为1的用户
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
sqlSession2.close();
}

看console:

DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 305414881.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@123442e1]
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
------->> User [id=1, username=王五, sex=2, birthday=null, address=null]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@123442e1]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@123442e1]
DEBUG [main] - Returned connection 305414881 to pool.
DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.5
------->> User [id=1, username=王五, sex=2, birthday=null, address=null]

2.sqlSession3的commit操作,清空二级缓存,再看发出的sql语句:

// 二级缓存测试
@Test
public void testCache2() throws Exception {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession();
SqlSession sqlSession3 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
// 第一次发起请求,查询id为1的用户
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
//这里执行关闭操作,将sqlsession中的数据写到二级缓存区域
sqlSession1.close(); //使用sqlSession3执行commit()操作
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
User user = userMapper3.findUserById(1);
user.setUsername("张明明");
userMapper3.updateUser(user);
//执行提交,清空UserMapper下边的二级缓存
sqlSession3.commit();
sqlSession3.close(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次发起请求,查询id为1的用户
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
sqlSession2.close();
}

看console:

DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1795834361.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
------->> User [id=1, username=王五, sex=2, birthday=null, address=null]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - Returned connection 1795834361 to pool.
DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.5
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Checked out connection 1795834361 from pool.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - ==> Preparing: update user set username=?,birthday=?,sex=?,address=? where id=?
DEBUG [main] - ==> Parameters: 张明明(String), null, 2(String), null, 1(Integer)
DEBUG [main] - <== Updates: 1
DEBUG [main] - Committing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - Returned connection 1795834361 to pool.
DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.3333333333333333
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Checked out connection 1795834361 from pool.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@6b0a41f9]
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
------->> User [id=1, username=张明明, sex=2, birthday=null, address=null]

关于二级缓存的一些配置参数:

1.useCache配置:

在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true

<select id="findOrderListResultMap" resultMap="ordersUserMap" useCache="false">

2.flushCache清空缓存:--一般执行完commit都需要刷新缓存(清空),这个默认true就行。

在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。

设置statement配置中的flushCache="true" 属性,默认情况下为true即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。

<insert id="insertUser" parameterType="cn.itcast.mybatis.po.User" flushCache="true">

三、mybatis整合ehcache:

ehcache是一个分布式缓存框架。

系统为了提高系统并发,性能、一般对系统进行分布式部署(集群部署方式)

分布式缓存原理:

Mybatis学习(6)动态加载、一二级缓存

不使用分布缓存,缓存的数据在各各服务单独存储,不方便系统 开发。所以要使用分布式缓存对缓存数据进行集中管理。

mybatis无法实现分布式缓存,需要和其它分布式缓存框架进行整合。

整合过程:

1)mybatis提供了cache接口,如果要实现自己的缓存逻辑,实现cache接口即可(mybatis自己实现的cache:public class PerpetualCache implements Cache )

加入ehcache包:ehcache-core.jar 和mybatis-ehcache.jar:

2)配置mapper.xml,使用ehcache的实现cache的实现类:

 <mapper namespace="com.cy.mapper.UserMapper">

     <!-- 开启本mapper的namespace下的二缓存
type:指定cache接口的实现类的类型,mybatis默认使用PerpetualCache
要和ehcache整合,需要配置type为ehcache实现cache接口的类型
-->
<cache type="org.mybatis.caches.ehcache.EhcacheCache"/>
....
</mapper>

3)加入ehcache的配置文件

在config source目录下加入ehcache.xml:

 <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../config/ehcache.xsd">
<diskStore path="F:\develop\ehcache" />
<defaultCache
maxElementsInMemory="1000"
maxElementsOnDisk="10000000"
eternal="false"
overflowToDisk="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
</defaultCache>
</ehcache>

测试代码:

    // 二级缓存测试
@Test
public void testCache2() throws Exception {
SqlSession sqlSession1 = sqlSessionFactory.openSession();
SqlSession sqlSession2 = sqlSessionFactory.openSession(); UserMapper userMapper1 = sqlSession1.getMapper(UserMapper.class);
User user1 = userMapper1.findUserById(1);
System.out.println(user1);
sqlSession1.close(); UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.findUserById(1);
System.out.println(user2);
sqlSession2.close();
}

看console打印,使用了缓存:

DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.0
DEBUG [main] - Opening JDBC Connection
DEBUG [main] - Created connection 1909716601.
DEBUG [main] - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@71d3f679]
DEBUG [main] - ==> Preparing: SELECT * FROM USER WHERE id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
------->> User [id=1, username=张明明, sex=2, birthday=null, address=null]
DEBUG [main] - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@71d3f679]
DEBUG [main] - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@71d3f679]
DEBUG [main] - Returned connection 1909716601 to pool.
DEBUG [main] - Cache Hit Ratio [com.cy.mapper.UserMapper]: 0.5
------->> User [id=1, username=张明明, sex=2, birthday=null, address=null]

四、mybatis二级缓存的应用场景和局限性:

应用场景:

对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。

实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。

局限性:

mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题需要在业务层根据需求对数据有针对性缓存。