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进行二级缓存的查询。
二级缓存开启后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。
二级缓存的的配置
-
让Mybatis支持二级缓存。在SqlMapConfig.xml添加配置:
<setting name="cacheEnabled" value="true"/>
默认值为true,所以不配置也行。 -
让当前的映射文件支持二级缓存。在IUserDao.xml中添加配置:
<cache/>
-
让当前的查询操作支持二级缓存。在对应的查询操作中添加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对象却不是同一个,这和一级缓存的结果不一样。从这里即可得知,一级缓存存储的是对象,二级缓存储存的是数据。当访问二级缓存的时候,现将数据封装称对象,再返回给请求者。