Mybatis学习第六弹。
Mybatis-6-延迟加载与缓存
1. 延迟加载
1. 什么是延迟加载
- 延迟加载机制是为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是当在真正需要数据的时候,才真正执行数据加载操作。
- 延迟加载(lazy load)是(也称为懒加载)。
2. 为什么要使用延迟加载
- 实际开发中一张表往往关联着其他表,但每次查询不一定要用到其他表。
- 查询一张表比查询多张表速度要快,可以大大提高查询速度。
3. 延迟加载存在的问题
由于是用到了数据才去加载,所以在用户大量使用查询的时候,会导致查询结果较慢。
4. 延迟加载应用示例
场景
Account和User表
表名 列名1 列名2 列名3 列名4 列名5 Account id uid money User id username birthday sex address User可以有多个Account(即是一对多)。
Account通过uid与唯一的User关联。
需求
- 在通过查询Account时,不直接查询出其中的User。
- 在需要使用User信息部分(例如:调用getUser模拟使用User信息)时,才实行加载。
分析
- Account中有User的属性用于关联User表查询的信息。
- 不调用其中的getUser方法的时候不查询User的数据(延迟加载)。
- 在Account的dao.xml配置返回值的时候将resultMap中的association添加select明确是使用延迟加载来查询User属性。
编码
给Account实体类中添加唯一User属性
1
2
3
4
5
6public class Account implements Serializable {
private int id;
private int uid;
private double money;
private User user; // 唯一user
}
在UserDao中添加根据id查询唯一User的抽象方法findById。
1
2
3public interface UserDao {
public User findById(Integer uid);
}
在AccountDao中添加findAll查询所有的方法。
1
2
3public interface AccountDao {
public List<Account> findAll();
}
在AccountDao.xml中Mapper下添加关联findAll查询方法的配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<mapper namespace="top.tobing.dao.AccountDao">
<resultMap id="accountMap" type="top.tobing.domain.Account">
<id column="id" property="id"/>
<result column="aid" property="id"/>
<result column="uid" property="uid"/>
<result column="money" property="money"/>
<association property="user" javaType="top.tobing.domain.User"
select="top.tobing.dao.UserDao.findById" column="uid">
</association>
</resultMap>
<select id="findAll" resultMap="accountMap">
select * from account;
</select>
</mapper>此步骤在association中包含了select,可以实现按需加载User。
在UserDao.xml中添加关联findById的方法。
1
2
3
4
5<mapper namespace="top.tobing.dao.UserDao">
<select id="findById" resultType="top.tobing.domain.User">
select * from user where id = #{uid};
</select>
</mapper>在测试方法中测试是否实现延迟加载
不调用getUser等,与User信息相关的方法。
1
2
3
4
5
6
7
public void findAllTest(){
List<Account> accounts = accountDao.findAll();
for (Account account : accounts) {
System.out.println(account.getMoney());
}
}查询结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
152019-11-28 12:18:36,346 224 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Opening JDBC Connection
2019-11-28 12:18:36,578 456 [ main] DEBUG source.pooled.PooledDataSource - Created connection 1038677529.
2019-11-28 12:18:36,578 456 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3de8f619]
2019-11-28 12:18:36,582 460 [ main] DEBUG .tobing.dao.AccountDao.findAll - ==> Preparing: select * from account;
2019-11-28 12:18:36,617 495 [ main] DEBUG .tobing.dao.AccountDao.findAll - ==> Parameters:
2019-11-28 12:18:36,673 551 [ main] DEBUG .tobing.dao.AccountDao.findAll - <== Total: 6
20000.0
40000.0
30000.0
21000.0
40000.0
50000.0
2019-11-28 12:18:36,675 553 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@3de8f619]
2019-11-28 12:18:36,675 553 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@3de8f619]
2019-11-28 12:18:36,676 554 [ main] DEBUG source.pooled.PooledDataSource - Returned connection 1038677529 to pool.只查询了一次。
> id为42时,调用getUser方法
1
2
3
4
5
6
7
8
9
10
public void findAllTest(){
List<Account> accounts = accountDao.findAll();
for (Account account : accounts) {
System.out.println(account.getMoney());
if(account.getUid()==42){ // 只有id为42才查询。
System.out.println(account.getUser());
}
}
}
> 只有id为42,才去执行查询User表(调用UserDao.xml中的findById)
1
2
3
4
5
6
User{id=42, username='Rongon', birthday=null, sex='男', address='江苏'}
30000.0
21000.0
40000.0
50000.0
User{id=42, username='Rongon', birthday=null, sex='男', address='江苏'}
5. 练习:以第4为例,实现查询用户User表,延迟加载用户的Account信息。
User表中添加关联的Account的字段。
1
2
3
4
5
6
7
8public class User {
private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;
private List<Account> accounts; // 用户对应的多个账号
}
AccountDao接口中添加按照uid查询多个Account的方法
1
2
3public interface AccountDao {
List<Account> findAllByUid(Integer uid);
}
UserDao接口中添加按照查询所有的方法\
1
2
3public interface UserDao {
public List<User> findAllAccount();
}
AccountDao.xml中添加AccountDao接口对应的方法的配置
1
2
3<select id="findAllByUid" resultMap="top.tobing.domain.Account">
select * from account where uid = #{id};
</select>
UserDao.xml中添加UserDao接口对应的方法配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<mapper namespace="top.tobing.dao.UserDao">
<resultMap id="userMap" type="top.tobing.domain.User">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="birthday" property="birthday"/>
<result column="address" property="address"/>
<result column="sex" property="sex"/>
<association property="accounts" javaType="List"
select="top.tobing.dao.AccountDao.findAllByUid" column="id">
</association>
</resultMap>
<select id="findAllAccount" resultMap="userMap">
select * from user;
</select>
</mapper>注意:
- association中的javaType是account对应的数据类型(List
) - association中的column参数是传给top/tobing/AccountDao.xml中的findAllByUid对应的mapper.
- association中的javaType是account对应的数据类型(List
检验是否实现延迟查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
212019-11-28 16:21:04,172 501 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Preparing: select * from user;
2019-11-28 16:21:04,207 536 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Parameters:
2019-11-28 16:21:04,261 590 [ main] DEBUG ing.dao.UserDao.findAllAccount - <== Total: 6
Tobing
男
==============================
Rongon
男
==============================
Zenyet
男
==============================
尼古拉斯
男
==============================
张无忌
男
==============================
张三丰
男
==============================1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
272019-11-28 16:21:44,570 217 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Opening JDBC Connection
2019-11-28 16:21:44,776 423 [ main] DEBUG source.pooled.PooledDataSource - Created connection 1314838582.
2019-11-28 16:21:44,776 423 [ main] DEBUG ansaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@4e5ed836]
2019-11-28 16:21:44,779 426 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Preparing: select * from user;
2019-11-28 16:21:44,816 463 [ main] DEBUG ing.dao.UserDao.findAllAccount - ==> Parameters:
2019-11-28 16:21:44,868 515 [ main] DEBUG ing.dao.UserDao.findAllAccount - <== Total: 6
Tobing
男
2019-11-28 16:21:44,869 516 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Preparing: select * from account where uid = ?;
2019-11-28 16:21:44,870 517 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Parameters: 41(Integer)
2019-11-28 16:21:44,871 518 [ main] DEBUG ng.dao.AccountDao.findAllByUid - <== Total: 1
[Account{id=1, uid=41, money=20000.0}]
==============================
Rongon
男
2019-11-28 16:21:44,880 527 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Preparing: select * from account where uid = ?;
2019-11-28 16:21:44,881 528 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Parameters: 42(Integer)
2019-11-28 16:21:44,882 529 [ main] DEBUG ng.dao.AccountDao.findAllByUid - <== Total: 2
[Account{id=2, uid=42, money=40000.0}, Account{id=6, uid=42, money=50000.0}]
==============================
Zenyet
男
2019-11-28 16:21:44,882 529 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Preparing: select * from account where uid = ?;
2019-11-28 16:21:44,883 530 [ main] DEBUG ng.dao.AccountDao.findAllByUid - ==> Parameters: 43(Integer)
2019-11-28 16:21:44,883 530 [ main] DEBUG ng.dao.AccountDao.findAllByUid - <== Total: 1
[Account{id=3, uid=43, money=30000.0}]
==============================
6. 总结
与之前的立即相比。延迟加载在要延迟加载的association中添加了select、column等属性。
select:用于指定调用那个方法执行查询延迟加载部分内容。
column:用于指定查询延迟加载部分的参数。
javaType:要合实体类一一对应。
所谓延迟加载,在此处的体现是,根据需求,决定是否调用其他的方法(该方法可以实现查询延迟加载的部分数据)。
2.Mybatis中的缓存
1. 缓存
- What:存储在内存中临时数据。
- Why:减少和数据库交互次数,提高执行效率。
- When:
- 使用缓存:数据经常查询、数据不经常改变、数据正确性对最终结果影响不大。
- 不使用缓存:数据经常改变、数据正确性对最终结果影响很大(例如:商品库存、银行汇率、股市牌价等)
2. Mybatis中缓存机制
- 一级缓存:
- 是指Mybatis中SqlSession对象的缓存
- 当我们执行查询后,查询的结果会存入到SqlSession对象提供的一块区域中(一个Map)。
- 当我们查询同样的数据时,Mybatis会先去SqlSession中的区域找,如果存在直接拿来用。
- 因为一级缓存保存在SqlSession,所以当SqlSession对象消失,一级缓存自动消失。
- 二级缓存:
- 是指存储在SqlSessionFactory对象的缓存。由同一个SqlSessionFactory对象创建的SqlSession共享其缓存。
- Mybatis默认不开启二级缓存
- SqlSession执行相同Mapper映射下sql,执行commit提交,将会清空Mapper映射下的二级缓存
3. 一级缓存
Mybatis中默认开启一级缓存
验证步骤
- 创建User、UserDao、UserDao.xml
1
2
3
4
5
6
7 public class User {
private int id;
private String username;
private Date birthday;
private String sex;
private String address;
}
UserDao中添加一个用于查询的查询方法
1
2
3 public interface UserDao {
public User findById(int id);
}
UserDao.xml配置查询的SQL语句
1
2
3
4
5 <mapper namespace="top.tobing.dao.UserDao">
<select id="findById" resultType="top.tobing.domain.User">
select * from user where id = #{id} ;
</select>
</mapper>验证缓存查询结果
1
2
3
4
5 public void findByIdTest(){
User user = userDao.findById(44); // 第一次查询
System.out.println(user);
User user1 = userDao.findById(44); // 第二次查询
System.out.println(user1);
1
2
3
4
5 DEBUG op.tobing.dao.UserDao.findById - ==> Preparing: select * from user where id = ? ;
DEBUG op.tobing.dao.UserDao.findById - ==> Parameters: 44(Integer)
DEBUG op.tobing.dao.UserDao.findById - <== Total: 1
top.tobing.domain.User@7fd7a283
top.tobing.domain.User@7fd7a283两次查询出来的对象地址相同
验证清楚缓存
调用clearCache、close等方法。
1
2
3
4
5
6
7
8
9
10
11
12 public void findByIdTest(){
User user = userDao.findById(44); // 第一次查询
System.out.println(user);
// 再次获取session
// session.close();
// session = factory.openSession();
// userDao = session.getMapper(UserDao.class);
// 另外一种方式清空数据
session.clearCache();
User user1 = userDao.findById(44); // 第二次查询
System.out.println(user1);结果查询了两次
调用了commit更新等操作。
1
2
3
4
5
6
7
8
9
10
public void firstCacheTest(){
User user = userDao.findById(44); // 第一次查询
System.out.println(user);
user.setUsername("CacheTest");
userDao.updateUser(user);
user = userDao.findById(44);
System.out.println(user);
System.out.println("--------------");
}此处也会跳过缓存,直接查询。
4. 二级缓存
同一个SqlSessionFactory得到的SqlSession可以共享二级缓存数据。
使用步骤:
- SqlMapConfig中开启二级缓存
- Mapper中开启二级缓存
- statement中使用二级缓存
SQLMapConfig开启二级缓存
1
2
3
4<!--开启二级缓存-->
<settings>
<setting name="cacheEnable" value="true"/>
</settings>
Mapper中开启二级缓存、statement使用二级缓存
1
2
3
4
5
6
7
8<mapper namespace="top.tobing.dao.UserDao">
<!--Mapper开启缓存-->
<cache></cache>
<!--查询时使用缓存-->
<select id="findById" resultType="top.tobing.domain.User" useCache="true">
select * from user where id = #{id} ;
</select>
</mapper>检验结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void secondCacheTest(){
SqlSession sqlSession1 = factory.openSession();
UserDao userDao1 = sqlSession1.getMapper(UserDao.class);
User user1 = userDao1.findById(44);
System.out.println(user1);
sqlSession1.close();
SqlSession sqlSession2 = factory.openSession();
UserDao userDao2 = sqlSession2.getMapper(UserDao.class);
User user2 = userDao2.findById(44);
System.out.println(user2);
sqlSession2.close();
}结果显示查询了一次。
5. 总结
- 缓存是可以数据库开销。
- Mybatis中有两种缓存:一级缓存和二级缓存
- 一级缓存作用范围在SqlSession中,SqlSession,commit(增删改)会清空缓存。
- 二级缓存作用范围在SqlSessionFactory,即是一个SqlSessionFactory生产出来的SqlSession共享同一二级缓存。
- 二级缓存会随着同一mapper任意一个SqlSession进行了commit而清空
- 一级缓存不需要手动开启。
- 二级缓存需要设置SqlMapConfig、Mapper、和要查询的方法。