mybatis探究之延迟加载和缓存
一、什么是延迟加载
1.延迟加载的概念
在mybatis进行多表查询时,并非所有的查询都需要立即进行。例如在查询带有账户信息的用户信息时,我们们并不需要总是在加载用户信息时就一定要加载他的账户信息。这时就要用到延迟加载,所谓延迟加载就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。延迟加载也称懒加载。
2.延迟加载的好处和坏处
好处:先从单表查询,需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快。
坏处:因为只有当需要用到数据时,才会进行数据库查询,这样在大批量数据查询时,因为查询工作也要消耗
时间,所以可能造成用户等待时间变长,造成用户体验下降。
3.什么时候使用延迟加载
在对应的四种表关系(一对多,多对一,一对一,多对多)中:
一对多,多对多:通常情况下我们都是采用延迟加载。
多对一,一对一:通常情况下我们都是采用立即加载。
二、一对一实现延迟加载
在进阶案例的基础上,进行如下修改:
1.添加延迟加载的配置
在主配置文件SqlMapConfig.xml文件中,添加settings标签,可以参考mybatis官方文档
<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
2.修改主从表对应关系的配置
将映射配置文件IAccountDao.xml文件中的resultMap标签中的association标签修改为如下:
<association property="user" javaType="domain.User"
select="dao.IUserDao.findById" column="uid"/>
select的内容是: 要调用的IUserDao中对应的 select 方法的 id ,column的内容是 : 要传递给 select 方法的参数在account数据表中对应的列名。
3.修改从表查询的SQL语句
在原来的findAllAccountsWithUser方法中,SQL语句直接将两个表进行笛卡尔积,如果不修改SQL语句,就会进行立即查询。因为是延迟加载,所以此处只需查询account信息即可。在映射配置文件IAccountDao.xml中进行如下修改:
<!-- 以延迟加载的方式配置查询带有用户信息的账户信息 -->
<select id="findAllAccountsWithUser" resultMap="accountUserMap">
select* from account
</select>
4.测试运行
1.采用立即加载方式的查询
过程:直接执行一条SQL语句就可以获得带有用户信息的账户信息
2.不修改测试函数进行查询
当我们完成上述配置之后,直接运行测试函数,会发现并没有实现延迟加载。每次查询账户时依旧对用户进行了查询:这是因为在测试函数当中,对查询到的account对象和user对象进行打印,相当每次查询都需要用到数据。而延迟加载是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。所以每次都会查询用户信息。
/**
* 测试以延迟加载的方式查询带有用户信息的账户信息
*
* @throws IOException
*/
@Test
public void testFindAllAccountsWithUser() throws IOException {
List<Account> accounts = accountDao.findAllAccountsWithUser();
for (Account account : accounts
) {
System.out.println(account);//这里表明每次查询都需要加载完整数据
System.out.println(account.getUser());
}
}
注意:即便是注释掉System.out.println(account.getUser()); 每次查询账户信息时,也会查询出用户信息。这是为什么呢?因为account对象中包含对user对象的引用,直接打印account对象,需要构造完整的account对象,也就需要从数据库查出user对象的属性,并通过反射赋值给account对象中的user。
3.修改测试函数进行查询
@Test
public void testFindAllAccountsWithUser() throws IOException {
List<Account> accounts = accountDao.findAllAccountsWithUser();
int i = 0;
for (Account account : accounts
) {
System.out.println(account.getMoney());
if(i == 1)
System.out.println(account.getUser());
i++;
}
}
当修改测试函数之后,每次遍历只需要打印出账户的金额信息,不需要完整的account对象,所以并没有对user表进行查询,而当计数变量i == 1时,需要打印user信息,这才对user表进行查询。
三、一对多实现延迟加载
1.修改主从表对应关系配置
将映射配置文件IUserDao.xml文件中的resultMap标签中的collection标签修改为如下:
<collection property="accounts" ofType="domain.Account"
select="dao.IAccountDao.findAccountsByUid" column="id"/>
2.添加查询方法
在IAccountDao接口中添加根据用户id查询账户信息的方法:
/**
* 根据用户id查询账户信息
* @param uid
* @return
*/
List<Account> findAccountsByUid(Integer uid);
3.配置查询方法
在映射配置文件IAccountDao.xml接口中配置根据用户id查询账户信息的查询方法:
<!-- 配置根据用户id查询账户信息 -->
<select id="findAccountsByUid" parameterType="Integer" resultType="domain.Account">
select * from account where uid = #{uid}
</select>
4.修改sql语句
在映射配置文件IUserDao.xml文件中进行如下修改:
<!-- 配置以延迟加载的方式查询带有账户信息的用户信息 -->
<select id="findUserWithAccounts" resultMap="userAccountsMap">
select * from user
</select>
5.测试运行
在测试类MybatisTest中修改 testFindUserWithAccounts方法为:
@Test
public void testFindUserWithAccounts() {
//6.执行操作
List<User> users = userDao.findUserWithAccounts();
int i = 0;
for(User user : users) {
System.out.println(user.getUsername());
if(i == 3)
System.out.println(user.getAccounts());
i++;
}
}
四、什么是缓存
1.缓存的概念
缓存就是在内存中存储的数据备份,当数据没有发生本质改变的时候,我们就不让数据的查询去数据库进行操作,而去内存中取数据,这样就大大降低了数据库的读写次数,而且从内存中读数据的速度比去数据库查询要快一些,这样同时又提高了效率。
2.缓存如何使用
在数据库中适用于缓存机制的数据包括:经常查询并且不经常改变的数据和结果的正确与否对最终结果影响不大的数据。相应地,不适用于缓存机制的数据包括:经常改变的数据和结果的正确与否对最终结果影响很大的数据,例如:商品的库存,银行的汇率,股市的牌价。
3.mybatis中的缓存机制
mybatis中的缓存根据缓存的生命周期,可分为一级缓存和二级缓存。Mybatis默认开启一级缓存而关闭二级缓存。
五、mybatis中的一级缓存
1.什么是一级缓存
一级缓存指的是Mybatis中SqlSession对象的缓存,当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。该区域的结构是一个Map。当我们再次查询同样的数据,Mybatis首先会去sqlsession中查询是否,如果该缓存中有这个数据,就直接从缓存中读取数据,否则就去数据库进行查询。当SqlSession对象消失(close)时,mybatis的一级缓存也就消失了。
2.一级缓存的测试
在测试类MybatisTest中添加如下测试方法:
/**
* 测试一级缓存
*/
@Test
public void testL1Cache() {
User user1 = userDao.findById(41);
System.out.println("第一次查询的用户:" + user1);
User user2 = userDao.findById(41);
System.out.println("第二次查询用户:" + user2);
System.out.print("第一次和第二次是否是同一对象:");
System.out.println(user1 == user2);
sqlSession.clearCache();//清空缓存
User user3 = userDao.findById(41);
System.out.println("第三次查询用户:" + user3);
System.out.print("第二次和第三次是否是同一对象:");
System.out.println(user2 == user3);
sqlSession.close();//close操作会清空缓存
//再次获取 SqlSession 对象
sqlSession = factory.openSession();
userDao = sqlSession.getMapper(IUserDao.class);
User user4 = userDao.findById(41);
System.out.println("第四次查询用户:" + user4);
System.out.print("第三次和第四次是否是同一对象:");
System.out.println(user3 == user4);
}
为了更清楚地看到结果,在User类的toString方法返回的字符串中添加super.toString()方法。运行结果如下:
可以看到第二次查询时,是直接从缓存获取,并非查询数据库。因此第一次和第二次都是同一对象。而第三次查询由于清空缓存,所以是从数据库中查询,所以不是同一对象。第四次由于关闭sqlSession对象,缓存消失,所以也是从数据库中查询。
3.一级缓存的分析
一级缓存是 SqlSession 范围的缓存,当调用 SqlSession 的修改,添加,删除,commit(),close()等方法时,就会清空一级缓存。
第一次发起查询用户id为1的用户信息,先去找缓存中是否有id为1的用户信息,如果没有,从数据库查询用户信息。得到用户信息后,将用户信息存储到一级缓存中。如果 sqlSession 去执行 commit 操作(对数据库执行插入、更新、删除),就会清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,避免脏读。(换句话说,只要对数据库进行更新、删除、插入等操作,就会清空一级缓存,避免缓存中数据和数据库中数据不一致。)
第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有id为1的用户信息,缓存中有,直接从缓存中获取用户信息。
六、mybatis中的二级缓存
1.什么是二级缓存
它指的是Mybatis中SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。多个 SqlSession 去操作同一个 Mapper 映射的 sql 语句,多个SqlSession 可以共用二级缓存,二级缓存是跨 SqlSession 的。
2.二级缓存实例
1.在主配置文件中开启全局二级缓存
二级缓存默认不开启,需要在主配置文件SqlMapConfig.xml文件中开启二级缓存的支持。
<settings>
<!-- 开启二级缓存的支持 -->
<setting name="cacheEnabled" value="true"/>
</settings>
2.指定要开启二级缓存的映射配置文件
在映射配置文件IUserDao.xml文件中添加:
<!-- cache标签仅用于指定要开启二级缓存的映射配置文件 -->
<cache></cache>
3.修改查询方法的配置
将 IUserDao.xml 映射配置文件中的select标签中设置 useCache=”true”代表当前这个 statement 要使用
二级缓存,如果不使用二级缓存可以设置为 false。
<!-- 配置根据id查询用户 -->
<select id="findById" resultType="domain.User" useCache="true">
select * from user where id = #{id};
</select>
注意:针对每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存。
4.测试
在测试类MybatisTest中添加测试方法:
/**
* 测试二级缓存
*/
@Test
public void testL2Cache() {
User user1 = userDao.findById(41);
System.out.println("第一次查询的用户:" + user1);
sqlSession.close(); //关闭一级缓存
SqlSession sqlSession2 = factory.openSession();
IUserDao userDao2 = sqlSession2.getMapper(IUserDao.class);
User user2 = userDao2.findById(41);
System.out.println("第二次查询用户:" + user2);
System.out.print("第一次和第二次是否是同一对象:");
System.out.println(user1 == user2);
sqlSession2.close();
}
可以看到虽然只查询了一次,第二次确实是从二级缓存中读取数据,但是第一次查询得到的对象和第二次查询得到的对象并不一致。
3.二级缓存分析
二级缓存中存放的对象的属性数据,而非对象数据。因此,即便从二级缓存读取数据,得到的对象也是不相同的。
在开启mybatis的二级缓存之后。如果sqlSession1去查询用户id为1的用户信息,查询到用户信息会将查询数据存储到二级缓存中。sqlSession2去查询用户id为1的用户信息,去缓存中找是否存在数据,如果存在直接从缓存中取出数据。如果SqlSession3去执行相同 mapper下sql,执行commit提交,清空该 mapper下的二级缓存区域的数据。
注意:当我们在使用二级缓存时,所缓存的类一定要实现 java.io.Serializable 接口,这种就可以使用序列化方式来保存对象。
------------恢复内容结束------------