myBatis缓存与动态SQL

概要:

  1. 1,2级缓存处理
  2. 动态化SQL

一、1,2级缓存处理


知识点:

1级缓存使用场景

2级缓存使用场景

1、1级缓存使用场景

订单表与会员表是存在一对多的关系,为了尽可能减少join 查询,进行了分阶段查询,即先查询出订单表,在根据member_id 字段查询出会员表,最后进行数据整合 。如果订单表中存在重复的member_id,就会出现很多没必要的重复查询。

​ 针对这种情况myBatis 通过1级缓存来实现,在同一次查询会话中如果出现相同的语句及参数,就会从缓存中取出不在走数据库查询。

​ 1级缓存只能作用于查询会话中 所以也叫做==会话缓存==。

示例:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public interface UserMapper {

User getUser(Integer id);

User selectById(Integer id);

@Update("select * from user where id = #{id}")
int updateById(Integer id);
}


@Slf4j
public class MainTest {
private SqlSession sqlSession = null;
private SqlSession sqlSession2 = null;
@Before
public void before() {
String resource = "mybatis-config.xml";
InputStream inputStream = MainTest.class.getClassLoader().getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
sqlSession = sqlSessionFactory.openSession();
sqlSession2 = sqlSessionFactory.openSession();

}

@Test
public void test00(){
//原始用法
User user = (User)sqlSession.selectOne("com.study.mapper.UserMapper.selectById", 1);
System.out.println(user);
}



@Test
public void test01(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUser(1);
log.info("首次查询======================");
User user2 = mapper.getUser(1);
User user3 = mapper.selectById(1);
System.out.println(user1);
System.out.println(user2);
System.out.println(user3);
System.out.println(user2==user1);
System.out.println(user3==user1);
}

@Test
public void test02(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserMapper mapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = mapper.getUser(1);
log.info("首次查询======================");
User user2 = mapper2.getUser(1);
System.out.println(user1);
System.out.println(user2);
System.out.println(user2==user1);
}

@Test
public void test03(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserMapper2 mapper2 = sqlSession.getMapper(UserMapper2.class);
User user1 = mapper.getUser(1);
log.info("首次查询结束======================");
User user2 = mapper2.getUser(1);
System.out.println(user1);
System.out.println(user2);
System.out.println(user2==user1);
}

@Test
public void test04(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUser(1);
log.info("首次查询======================");
sqlSession.clearCache();
User user2 = mapper.getUser(1);
System.out.println(user1);
System.out.println(user2);
System.out.println(user2==user1);
}

@Test
public void test05(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.getUser(1);
log.info("首次查询======================");
mapper.updateById(1);
User user2 = mapper.getUser(1);
System.out.println(user1);
System.out.println(user2);
System.out.println(user2==user1);
}

一级缓存的使用条件:

1.必须是相同的SQL和参数

2.必须是相同的statement 即同一个mapper实例接口中的同一个方法

3.必须是相同的会话

4.查询语句中间没有执行session.clearCache() 方法

5.查询语句中间没有执行 insert update delete 方法

(无论变动记录是否与 缓存数据有无关系)

6.必须是相同的namespace 即同一个mapper

7、同一个Mapper实例(Mapper类型相同的不同实例也不行)

1
2
3
//无法共用一级缓存
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
UserMapper2 mapper2 = sqlSession.getMapper(UserMapper2.class);

一级缓存源码解析:

缓存获取 :

1
2
3
4
5
6
7
8
mapper.mapper.selectById(23)
>org.apache.ibatis.binding.MapperProxy#invoke
>org.apache.ibatis.binding.MapperMethod#execute
>org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne()
>org.apache.ibatis.session.defaults.DefaultSqlSession#selectList()
>org.apache.ibatis.executor.CachingExecutor#query()//尝试获取二级缓存
>org.apache.ibatis.executor.BaseExecutor#query() 142L
>org.apache.ibatis.cache.impl.PerpetualCache#getObject 55L//尝试获取一级缓存

缓存的存储:

1
2
3
4
5
6
mapper.mapper.selectById(23)
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList()
org.apache.ibatis.executor.CachingExecutor#query()
org.apache.ibatis.executor.BaseExecutor#query() 142L
org.apache.ibatis.executor.BaseExecutor#queryFromDatabase
org.apache.ibatis.cache.impl.PerpetualCache#putObject

image-20210509213336367

通过对clearCache 作为入口我们可能追踪到 一级缓存的实现PerpetualCache

1
2
3
4
org.apache.ibatis.session.defaults.DefaultSqlSession#clearCache
org.apache.ibatis.executor.CachingExecutor#clearLocalCache
org.apache.ibatis.executor.BaseExecutor#clearLocalCache
org.apache.ibatis.cache.impl.PerpetualCache#clear

8.一级缓存是线程不安全的

提问:

在查询时另一个会话并发去修改查询的数据,一级缓存是否会生效?如果生效是否就会导致数据不正确?

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
27
28
29
30
31
32
33
@Test
public void testThread(){
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
new Thread(new Runnable() {
@Override
public void run() {
User user1 = userMapper.getUser(1);
System.out.println("线程一:");
System.out.println(user1);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程一:");
System.out.println(user1);
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
User user2 = userMapper.getUser(1);
user2.setUsername("周树人");
System.out.println("线程二:");
System.out.println(user2);
sqlSession.close();
}
}).start();

答:多次查询相同id一级缓存获取到的对象都是同一个应用,并没有做深克隆。所以当线程2查询某个缓存对象后并且人为修改了这个对象,那么也会同步影响线程一原本的对象,存在脏读问题

2、2级缓存使用场景:

业务系统中存在很多的静态数据如,字典表、菜单表、权限表等,这些数据的特性是不会轻易修改但又是查询的热点数据。一级缓存针对的是同一个会话当中相同SQL,并不适合这情热点数据的缓存场景。为了解决这个问题引入了二级缓存,它脱离于会话之外。

2级缓存示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@CacheNamespace()
public interface LabelMapper {
@Select("select * from t_label where id =#{id}")
Label getById(Integer id);
}
属性说明:
@CacheNamespace(
implementation = PerpetualCache.class,// 缓存实现 Cache接口 实现类
eviction = LruCache.class,// 缓存算法
flushInterval =60000,// 刷新间隔时间 毫秒
size =1024,// 最大缓存引用对象
readWrite =true,// 是否可写
blocking = false// 是否阻塞,用来防止缓存击穿
)
或者
<cache readOnly="false" blocking="true" flushInterval="60000" size="1024" eviction="LRU"/>

image-20210510232548387

image-20210510232254209bolocking 示意图

1、2级缓存使用条件:

1、当会话提交或关闭之后才会填充二级缓存

2、必须是在同一个命名空间之下

3、必须是相同的statement 即同一个mapper 接口中的同一个方法

4、必须是相同的SQL语句和参数

5、如果readWrite=true(readOnly=”false”) ,实体对像必须实现Serializable 接口

​ true:表示深克隆,实体需实现序列化,2次查询出来对象不是同一个,readWrite=false时,两次查询获取对象是同一个

6、mapper.xml 的<cache/>和 Mapper.java 的@CacheNamespace 是两个缓存空间,且不能共存

2、2级缓存清除条件:

1、xml中配置的update 不能清空 @CacheNamespace 中的缓存数据

2、只有修改会话提交之后 才会执行清空操作

3、任何一种增删改操作 都会清空整个namespace 中的缓存

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<cache  readOnly="true" blocking="true" flushInterval="60000" size="1024" eviction="LRU"/>

@Test
public void test00(){

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUser(1);
System.out.println("第一次会话结束");
sqlSession.close();

UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUser(1);
System.out.println(user==user2); //二级缓存生效,浅克隆,返回true
}

@Test
public void test01(){

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUser(1);
System.out.println("第一次会话结束");


UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user2 = userMapper2.getUser(1);
System.out.println(user==user2);//二级缓存不生效,返回false
}


@Test
public void test02(){

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
User user = userMapper.getUser(1);
System.out.println("第一次会话结束");
sqlSession.close();

UserMapper userMapper2 = sqlSession2.getMapper(UserMapper.class);
User user1 = new User();
user1.setUsername("lao li");
user1.setId(1);
userMapper2.updateUserById(user1);
//必须关闭,才能触发清空二级缓存
sqlSession2.close();
User user2 = sqlSessionFactory.openSession().getMapper(UserMapper.class).getUser(1);
System.out.println(user==user2);//二级缓存清空,false
}

2级缓存源码解析:

清除缓存!

1
2
3
4
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList() 147L
org.apache.ibatis.executor.CachingExecutor#query()81L
org.apache.ibatis.executor.CachingExecutor#query()95L
org.apache.ibatis.executor.CachingExecutor#flushCacheIfRequired() 164L //清除缓存

获取缓存关键源码!

1
2
3
4
5
6
7
8
org.apache.ibatis.cache.TransactionalCacheManager#getObject
org.apache.ibatis.cache.decorators.TransactionalCache#getObject
org.apache.ibatis.cache.decorators.SynchronizedCache#getObject
org.apache.ibatis.cache.decorators.LoggingCache#getObject
org.apache.ibatis.cache.decorators.SerializedCache#getObject
org.apache.ibatis.cache.decorators.ScheduledCache#getObject
org.apache.ibatis.cache.decorators.LruCache#getObject
org.apache.ibatis.cache.impl.PerpetualCache#getObject

保存2级缓存 !

1
2
3
4
5
6
7
8
9
10
11
org.apache.ibatis.executor.CachingExecutor#close
org.apache.ibatis.cache.TransactionalCacheManager#commit
org.apache.ibatis.cache.decorators.TransactionalCache#flushPendingEntries
org.apache.ibatis.cache.decorators.SynchronizedCache#putObject
org.apache.ibatis.cache.decorators.LoggingCache#putObject
org.apache.ibatis.cache.decorators.SerializedCache#putObject
org.apache.ibatis.cache.decorators.ScheduledCache#putObject
org.apache.ibatis.cache.decorators.LruCache#putObject
org.apache.ibatis.cache.impl.PerpetualCache#putObject


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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class TransactionalCache implements Cache {
private Cache delegate;
private Map<Object, Object> entriesToAddOnCommit;
@Override
public Object getObject(Object key) {
// issue #116
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
// issue #146
if (clearOnCommit) {
return null;
} else {
return object;
}
}
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}

public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}

public void rollback() {
unlockMissedEntries();
reset();
}

}

3、如何理解 TransacionCache

TransacionCache的生命周期是会话(事务)级别

TransacionCache:事务级别,sqlSession提交时会触发TransacionCache的commit

二级缓存:Mapper级别(应用级别)

一级缓存:SqlSession级别(会话级别)

问题:

为什么存在 TransactionalCache,且二级缓存必须是会话提交后才生效

  1. 为了防止事务回滚的场景,别的会话脏读了二级缓存的的内容
  2. 所以当本次会话未关闭前,二级缓存新增对象都暂存在entriesToAddOnCommit本地中
  3. 当事务提交时,才触发entriesToAddOnCommit 持久化到应用生命周期的delegate这个实际cache中
  4. 而且每个会话期间,获取二级缓存都是从delegate这个实际cache中获取,这样就避免了脏读(读未提交)问题.

image-20210512231601527

4、二级缓存生命周期示意图

image-20210512231634979

二、动态化SQL


基本命令使用

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

示例说明:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<trim prefix="where" prefixOverrides="and|or">
<if test="id != null">
and id = #{id}
</if>
<if test="name != null">
and name = #{name}
</if>
</trim>
trim属性说明:
* prefix="where"// 前缀
* prefixOverrides="and|or"// 前缀要替换的词
* suffix=""// 添加后缀
* suffixOverrides=""// 后缀要替换的词

<where>元素说明:
在where 包裹的SQL前会自动添加 where 字符 并去掉首尾多佘的 and|or 字符 相当于下配置:
<trim prefix="where" prefixOverrides="and|or" suffixOverrides="and|or">

<set>元素说明:
在set包裹的SQL前会自动添加 set 字符并去掉首尾多佘的 , 字符。
<update id="updateUser" parameterType="User">
UPDATE users
<set>
<if test="username != null">username=#{username},</if>
<if test="password != null">password=#{password},</if>
<if test="email != null">email=#{email},</if>
</set>
WHERE id=#{id}
</update>
<sql> 元素说明:在同一个mapper多个statement 存在多个相同的sql 片段时,可以通过<sql>元素声明,在通过   <include> 元素进行引用
声明sql 段
<sql id="files">
id ,name ,createTime
</sql>

引用
<include refid="files" />



<bind> 变量使用
有时需要进行一些额外 逻辑运行,通过 声明<bind>元素,并在其value 属性中添加运算脚本,如下示例 自动给likeName 加上了% 分号,然后就可以用#{likeName} 来使用带%分号的like 运算。

<bind name="likeName" value="'%'+ _parameter.getName() +'%'"></bind>



内置变量
_databaseid 数据库标识ID
_parameter 当前参数变理

使用别的解析模板

以上的if trim where 等逻辑符都是 myBatis 自带的XMLLanguageDriver 所提供的解释语言,除非此之外 我们还可以使用 MyBatis-Velocity 或 mybatis-freemarker 等外部 解释器来编写动态脚本。

mybatis-freemarker 使用

引入mybatis 包:

1
2
3
4
5
<dependency>
<groupId>org.mybatis.scripting</groupId>
<artifactId>mybatis-freemarker</artifactId>
<version>1.1.2</version>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
添加sql 语句
<select id="selectByIds"
resultType="com.tuling.mybatis.dao.User"
lang="org.mybatis.scripting.freemarker.FreeMarkerLanguageDriver">
select * from user
where id in(${ids?join(',')})
</select>


lang="org.mybatis.scripting.freemarker.FreeMarkerLanguageDriver"
指定解析器

添加接口方法
List selectByIds(@Param(“ids”) List ids);