延迟加载
举个例子:如果查询订单并且关联查询用户信息。如果先查询订单信息即可满足要求,当我们需要查询用户信息时再查询用户信息。把对用户信息的按需去查询就是延迟加载。 所以延迟加载即先从单表查询、需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
我们来对比一下:
关联查询:SELECT orders.*, user.name FROM orders, USER WHERE orders.user_id
= user.id
延迟加载相当于:
SELECT orders.*,
(SELECT name FROM USER WHERE orders.user_id = user.id)username FROM orders
所以这就比较直观了,也就是说,我把关联查询分两次来做,而不是一次性查出所有的。第一步只查询单表orders,必然会查出orders中的一个user_id字段,然后我再根据这个user_id查user表,也是单表查询。下面来总结一下如何使用这个延迟加载.
2. 使用association实现延迟加载
resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),其实association和collection还具备延迟加载的功能,这里我就拿association来说明,collection和association使用的方法都是一样的。需求就是上面提到的,查询订单并且关联查询用户,查询用户使用延迟加载。
由上面的分析可知,延迟加载要查询两次,第二次是按需查询,之前一对一关联查询的时候只需要查一次,把订单和用户信息都查出来了,所以只要写一个mapper即可,但是延迟加载查两次,所以理所当然要有两个mapper。
2.1 两个mapper.xml
需要定义两个mapper的方法对应的statement。先来分析一下思路:
1. 只查询订单信息的statement:
在UserMapperOrders.xml中:
- 只查询订单信息的statement,使用resultMap
- 通过查询到的订单信息中的user_id去查询用户信息的statement,得到用户
- 定义的resultMap将两者关联起来,即用订单信息user_id去查用户
<resultMap type="com.cx.pojo.Orders" id="OrdersUserLazyLoadingResultMap">
<!-- 配置映射订单信息 -->
<id column="id" property="id"/>
<result column="user_id" property="userId"/>
<result column="number" property="number"/>
<result column="createtime" property="createtime"/>
<result column="note" property="note"/>
<!-- 配置映射的关联的用户信息 -->
<!-- association用于映射关联查询单个对象的信息
property:要将关联查询的用户信息映射到Orders中的哪个属性
javaType:该属性的类型
-->
<association property="user" javaType="com.cx.pojo.User" select="com.cx.mapper.UserMapper.findById" column="user_id">
</association>
</resultMap>
<select id="findOrdersUserLazyLoading" resultMap="OrdersUserLazyLoadingResultMap">
SELECT * FROM orders
</select>
只查询用户信息的statement:
在UserMapper.xml中:
<select id="findById" parameterType="int" resultType="com.cx.pojo.User">
SELECT * FROM User WHERE id=#{id}
</select>
2.2 延迟加载的配置
在mybatis-config.xml中:
<settings>
<!--全局性设置懒加载。如果设为‘false’,则所有相关联的都会被初始化加载,默认值为false-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当设置为‘true’的时候,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载。默认值为true-->
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
2.3 UserMapperOrders.java
public interface UserMapperOrders {
public List<OrdersCustom> findOrdersUser() throws Exception;
//查询订单,关联查询用户信息,使用resultMap
public List<Orders> findOrdersUserResultMap() throws Exception;
//查询订单(关联用户)及订单明细
public List<Orders> findOrdersAndOrderDetailResultMap() throws Exception;
//查询用户购买商品信息
public List<User> findUserAndItemsResultMap() throws Exception;
//查询订单,关联用户查询,用户查询用的是延迟加载
public List<Orders> findOrdersUserLazyLoading() throws Exception;
}
测试:
@Test
public void testFindOrdersUserLazyLoading() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapperOrders userMapperOrders = sqlSession.getMapper(UserMapperOrders.class);
//查询订单表(单表)
List<Orders> list = userMapperOrders.findOrdersUserLazyLoading();
//遍历上边的订单列表
for(Orders orders : list) {
//执行getUser()去查询用户信息,这里实现按需加载
User user = orders.getUser();
System.out.println(user.getName());
}
}
一级缓存
缓存的作用是减轻数据库的压力,提高数据库的性能的。mybatis中提供了一级缓存和二级缓存。
从图中可以看出:
- 一级缓存是SqlSession级别的缓存。在操作数据库时需要构造sqlSession对象,在对象中有一个数据结构(HashMap)用于存储缓存数据。不同的sqlSession之间的缓存数据区域(HashMap)是互相不影响的。
- 二级缓存是mapper级别的缓存,多个SqlSession去操作同一个Mapper的sql语句,多个SqlSession可以共用二级缓存,二级缓存是跨SqlSession的。
一级缓存的工作原理:
从图中可以看出:第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息,将用户信息存储到一级缓存中。
如果中间sqlSession去执行commit操作(执行插入、更新、删除),则会清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
第二次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。
测试方法:
@Test
public void testCache1() throws Exception {
SqlSession sqlSession = sqlSessionFactory.openSession();//创建代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//下边查询使用一个SqlSession
//第一次发起请求,查询id为1的用户
User user1 = userMapper.findById(1);
System.out.println(user1.getName());
// 如果sqlSession去执行commit操作(执行插入、更新、删除),清空SqlSession中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。
//更新user1的信息
user1.setName("测试用户22");
userMapper.update(user1);
//执行commit操作去清空缓存
sqlSession.commit();
//第二次发起请求,查询id为1的用户
User user2 = userMapper.findById(1);
System.out.println(user2.getName());
sqlSession.close();
}
二级缓存
1. 二级缓存的原理
不同的mapper都有一个二级缓存,也就是说,不同的mapper之间的二级缓存是互不影响的。
从图中可以看出:
缓存的执行原理和前面提到的一级缓存是差不多的,二级缓存与一级缓存区别在于二级缓存的范围更大,多个sqlSession可以共享一个mapper中的二级缓存区域。mybatis是如何区分不同mapper的二级缓存区域呢?它是按照不同mapper有不同的namespace来区分的,也就是说,如果两个mapper的namespace相同,即使是两个mapper,那么这两个mapper中执行sql查询到的数据也将存在相同的二级缓存区域中。
2. 二级缓存的使用
2.1 开启二级缓存
由于mybaits的二级缓存是mapper范围级别,所以除了在mybatis-config.xml设置二级缓存的总开关外,还要在具体的mapper.xml中开启二级缓存。
- sqlSession1去查询用户id为1的用户信息,查询到用户信息会将查询数据存储到该UserMapper的二级缓存中。
- 如果SqlSession3去执行相同 mapper下sql,执行commit提交,则会清空该UserMapper下二级缓存区域的数据。
- sqlSession2去查询用户id为1的用户信息,去缓存中找是否存在数据,如果存在直接从缓存中取出数据。
<settings>
<!--全局性设置懒加载。如果设为‘false’,则所有相关联的都会被初始化加载,默认值为false-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--当设置为‘true’的时候,懒加载的对象可能被任何懒属性全部加载。否则,每个属性都按需加载。默认值为true-->
<setting name="aggressiveLazyLoading" value="false"/>
<setting name="cacheEnabled" value="true"/>
</settings>
还得在具体的mapper.xml中设置:
可以看到,具体的mapper中仅仅就一个<cache>
标签,并没有配置啥东西,这是因为mybatis中有默认的实现,我们如果不配置,那么就默认使用那个默认的实现。在mybatis的核心包里有cache的接口和这个默认的实现.
mybatis中也就只有这一个默认实现类,如果不使用mybatis的默认二级缓存的话,就需要自己实现cache接口,然后再在mapper.xml中配置一下了.
2.2 将po类实现Serializable接口
开启了二级缓存后,还需要将要缓存的pojo实现Serializable接口,为了将缓存数据取出执行反序列化操作,因为二级缓存数据存储介质多种多样,不一定只存在内存中,有可能存在硬盘中,如果我们要再取这个缓存的话,就需要反序列化了。所以建议mybatis中的pojo都去实现Serializable接口。
user.java
package com.cx.pojo;
import java.io.Serializable;
import java.util.List;
public class User implements Serializable{
private static final long serialVersionUID=121321312312321L;
private int id;
private String name;
private int age;
private List<Orders> ordersList;
public List<Orders> getOrdersList() {
return ordersList;
}
public void setOrdersList(List<Orders> ordersList) {
this.ordersList = ordersList;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
测试:
@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.findById(1);
System.out.println(user1.getName());
//这里执行关闭操作,将sqlsession中的数据写到二级缓存区域
sqlSession1.close();
//sqlSession3用来清空缓存的,如果要测试二级缓存,需要把该部分注释掉
//使用sqlSession3执行commit()操作
UserMapper userMapper3 = sqlSession3.getMapper(UserMapper.class);
User user = userMapper3.findById(1);
user.setName("chenxiang");
userMapper3.update(user);
//执行提交,清空UserMapper下边的二级缓存
sqlSession3.commit();
sqlSession3.close();
UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
// 第二次发起请求,查询id为1的用户
User user2 = userMapper2.findById(1);
System.out.println(user2.getName());
sqlSession2.close();
}
2.3 其他配置(useCache和flushCache)
mybatis中还可以配置userCache和flushCache等配置项,userCache是用来设置是否禁用二级缓存的,在statement中设置useCache=false可以禁用当前select语句的二级缓存,即每次查询都会发出sql去查询,默认情况是true,即该sql使用二级缓存。
<select id="findOrderListResultMap" resultMap="ordersUserMap" useCache="false">
这种情况是针对每次查询都需要最新的数据sql,要设置成useCache=false,禁用二级缓存,直接从数据库中获取。
在mapper的同一个namespace中,如果有其它insert、update、delete操作数据后需要刷新缓存,如果不执行刷新缓存会出现脏读。 设置statement配置中的flushCache=”true” 属性,默认情况下为true,即刷新缓存,如果改成false则不会刷新。使用缓存时如果手动修改数据库表中的查询数据会出现脏读。
<insert id="insertUser" parameterType="User" flushCache="true">
一般下执行完commit操作都需要刷新缓存,flushCache=true表示刷新缓存,这样可以避免数据库脏读。所以我们不用设置,默认即可。
3. MyBatis整合ehcache分布式缓存框架
3.1 问题的由来
上面的部分主要总结了一下mybatis中二级缓存的使用,但是mybatis中默认自带的二级缓存有个弊端,即无法实现分布式缓存,什么意思呢?就是说缓存的数据在自己的服务器上,假设现在有两个服务器A和B,用户访问的时候访问了A服务器,查询后的缓存就会放在A服务器上,假设现在有个用户访问的是B服务器,那么他在B服务器上就无法获取刚刚那个缓存,如下图所示:
所以我们为了解决这个问题,就得找一个分布式的缓存,专门用来存储缓存数据的,这样不同的服务器要缓存数据都往它那里存,取缓存数据也从它那里取,如下图所示:
这样就能解决上面所说的问题,为了提高系统并发性能、我们一般对系统进行上面这种分布式部署(集群部署方式),所以要使用分布式缓存对缓存数据进行集中管理。但是mybatis无法实现分布式缓存,需要和其它分布式缓存框架进行整合,这里主要介绍ehcache。
3.2 整合方法
mybatis提供了一个cache接口,如果要实现自己的缓存逻辑,实现cache接口开发即可。mybatis本身默认实现了一个,但是这个缓存的实现无法实现分布式缓存,所以我们要自己来实现。ehcache分布式缓存就可以,mybatis提供了一个针对cache接口的ehcache实现类,这个类在mybatis和ehcache的整合包中。
在pom.xml中加入:
<!-- ehcache 相关依赖 -->
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-ehcache -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-ehcache</artifactId>
<version>1.0.0</version>
</dependency>
再将该类的完全限定名写到type属性中即可
配置完成,现在mybatis就会自动去执行这个ehcache实现类了,就不会使用自己默认的二级缓存了,但是使用ehcache还有一个缓存配置别忘了,在classpath下新建一个ehcache.xml文件:
<ehcache>
<diskStore path="D:\develop\ehcache"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
</ehcache>
这样配置就好了。
4. 二级缓存的应用场景和局限性
对于访问多的查询请求且用户对查询结果实时性要求不高,此时可采用mybatis二级缓存技术降低数据库访问量,提高访问速度,业务场景比如:耗时较高的统计分析sql、电话账单查询sql等。实现方法如下:通过设置刷新间隔时间,由mybatis每隔一段时间自动清空缓存,根据数据变化频率设置缓存刷新间隔flushInterval,比如设置为30分钟、60分钟、24小时等,根据需求而定。
mybatis二级缓存对细粒度的数据级别的缓存实现不好,比如如下需求:对商品信息进行缓存,由于商品信息查询访问量大,但是要求用户每次都能查询最新的商品信息,此时如果使用mybatis的二级缓存就无法实现当一个商品变化时只刷新该商品的缓存信息而不刷新其它商品的信息,因为mybaits的二级缓存区域以mapper为单位划分的,当一个商品信息变化会将所有商品信息的缓存数据全部清空。解决此类问题可能需要在业务层根据需求对数据有针对性缓存。