首先回顾一下(一)的内容
重用执行器 Reuse Executor
Q:一个SqlSession执行了selectList和selectOne,两个的sql语句都相同,是否可以命中Statement的缓存?

A:可以命中缓存
所命中的缓存是JDBC中的ReuseStatement的PrepareStatement的缓存,不是一级缓存,只要最终的SQL语句一样就会命中Statement
UserMapper:
@CacheNamespace
public interface UserMapper {
//两个SQL一样
@Select({" select * from users where id=#{1}"})
User selectByid(Integer id);
@Select({" select * from users where id=#{1}"})
User selectByid3(Integer id);
}
@Test
public void sessionByReuseTest(){
SqlSession sqlSession = factory.openSession(ExecutorType.REUSE, true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//两个select对应两个不同的 MappedStatement 映射,会不会采用一个预处理器Statement?
//(JDBC的Statement)
mapper.selectByid(10);
mapper.selectByid3(10);
}
题目所说的Statement指的是JDBC的Statement

可以看到只引入了一次预编译
如果说两个SQL不一样
@CacheNamespace
public interface UserMapper {
//两个SQL一样
@Select({" select * from users where id=#{1}"})
User selectByid(Integer id);
@Select({" select * from users where id=#{1} and 1=1"})
User selectByid3(Integer id);
}
//重用执行器
@Test
public void sessionByReuseTest(){
SqlSession sqlSession = factory.openSession(ExecutorType.REUSE, true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//两个select对应两个不同的 MapperStatement,不会会采用一个预处理器Statement?
mapper.selectByid(10);
mapper.selectByid3(10);
}

可以看到引入了两次预编译
结论:只要是SQL语句相同,就会重复利用Statement,不论是doQuery还是doUpdate最终都还是会重复的使用Statement
查看源代码可以看到:

String是他的SQL语句
Statement就是他缓存的JDBC的Statement
查看doQuery源码
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(this.wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//预编译SQL语句
Statement stmt = this.prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
}
在预编译过程prepareStatement中(对应jdbc中的prepareStatement)

如果没有就通过StatementHandler构建一个新的Statement,之后put进缓存中
总结:在会话期间内,所有相同的SQL语句会采用同样的Statement,只有会话结束之后Statement才会一起关闭
批处理执行器

会话把所有的请求都发往Batch Executor,而Batch Executor会把所有的SQL语句保存起来到Statement集合中(缓存),等到flushStatement时一起进行处理(所有的增删改同时进行处理,不管StatementID是否一样),但是不一定都采用同一个Statement
@Test
public void sessionBatchTest(){
SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.setName(10,"道友友益永存");
mapper.addUser(Mock.newUser());
sqlSession.flushStatements();
}
Q:是否会使用同一个JDBC Statement?
A:不是同一个Statement
Q:set和add操作是一起提交的么?
A:是一起提交的
验证:此时断点查看

此时数据还没有提交

继续执行后查看数据库

在我们调用setName和addUser时,在MyBatis层面不会真正的执行,而是把他们转换为对应的JDBC Statement并保存在暂存区中,最后再一个个的执行其中的语句
@Test
public void sessionBatchTest1(){
SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.setName(10,"道友友益永存");
mapper.addUser(Mock.newUser());
mapper.addUser(Mock.newUser());
mapper.addUser(Mock.newUser());
mapper.setName(12,"道友友益永存");
//执行后batchResults有几个结果?
// A:2个 B:5个 C:3个
sqlSession.flushStatements();
}
Q:执行后batchResults有几个结果?
A:3个

Q:为什么是3个结果?什么情况下会采用同一个Statement?
1.sql 相同
2.MappedStatement相同
3.必须是连续的
在三个条件都满足的情况下才会使用同一个 JDBC statement
mapper.setName(10,"道友友谊永存");和mapper.setName(12,"道友友益永存");两者满足1、2条件不满足3条件,所以用了两个JDBC Statement
mapper.addUser(user);1、2、3条件都满足使用一个JDBC的Statement
所以最后共使用了3个JDBC的Statement

如果按照这样的顺序,则结果为四个
@Test
public void sessionBatchTest1(){
SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
mapper.setName(10,"道友友益永存");
mapper.addUser(Mock.newUser());
mapper.addUser(Mock.newUser());
mapper.setName(12,"道友友益永存");
mapper.addUser(Mock.newUser());
List<BatchResult> batchResults = sqlSession.flushStatements();
}


Q:为什么他不能像重用执行器一样,只要是相同的SQL语句就重用Statement从而减少编译?
A:如果按照重用执行器来执行如下代码
// 1.添加
// 2.执行
// 3.返回结果
@Test
public void sessionBatchTest(){
SqlSession sqlSession = factory.openSession(ExecutorType.BATCH,true);
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//MappedStatement
// 1.sql 相同 2.MappedStatement 3.必须是连续的 =》JDBC statement
mapper.setName(10,"道友友谊永存"); //添加 statement 把参数和SQL设置好
User user = Mock.newUser();
mapper.addUser(user);
mapper.setName(user.getId(),"小鲁班");
// JDBC statement?
// 一起提交的吗?
List<BatchResult> batchResults = sqlSession.flushStatements();
// A:2个 B:5个 C:3个
}
那么两个mapper.setName就会合并,但是mapper.addUser(user);还没有执行,也就是说第二个mapper.setName中的user.getId()为null,最后执行出错
每次执行JDBC操作时,都会添加 statement 把参数和SQL设置好,保存在Statement中。如果是相邻的那么自然就会重用Statement,再调用addBatch完成批处理的添加操作
下面进行断点查看原理
1.添加

mapper.setName(10,"道友友谊永存");,在doupdate()中进行 第一次添加

执行mapper.addUser(user);,在doupdate()中进行 第二次添加

依然进行创建

执行mapper.setName(user.getId(),"小鲁班");

同样是添加操作

所以最后我们可以看到在StatementList中有3个元素

在batchResultList中也有3个元素

此时我们的Statement还没有真正执行修改操作
在执行sqlSession.flushStatements();进而调用doFlushStatements时才会真正执行修改操作

Q:在多线程条件下去执行BatchExecutor是否会产生问题?-- 可以跟面试官聊聊


A:会产生问题,所以Executor不是线程安全的,不能进行跨线程的使用
注意:无论是否重用Statement,对于StatementHandler来说都是新的

Executor执行体系总结


一级缓存

什么情况下会命中一级缓存?
(一级缓存默认打开)存储接口为key-value(HashMap)

与两个参数有关
- 运行时参数相关
- 操作与配置相关
运行时参数相关
(1)sql 和参数必须相同
总结
public class FirstCacheTest {
private SqlSessionFactory factory;
private SqlSession sqlSession;
@Before
public void init() throws SQLException {
// 获取构建器
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
// 解析XML 并构造会话工厂
factory = factoryBuilder.build(ExecutorTest.class.getResourceAsStream("/mybatis-config.xml"));
sqlSession = factory.openSession();
}
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
User user1 =mapper.selectByid(10);
System.out.println(user == user1);
}
}
命中缓存

(2)必须是相同的statementID
如果SQL相同,参数不同则不会命中 一级缓存
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
User user1 =mapper.selectByid(12);
System.out.println(user == user1);
}

如果 sql 和参数都相同,但方法名不相同同样无法命中一级缓存
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
//statemetID = org.coderead.mybatis.UserMapper.selectByid
User user = mapper.selectByid(10);
//statemetID = org.coderead.mybatis.UserMapper.selectByid3
User user1 =mapper.selectByid3(10);
System.out.println(user == user1);
}

(3)sqlSession必须一样 (会话级缓存)
如果 sql 和参数相同,statementID也相同,但是SqlSession不同,也不会命中一级缓存
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
User user1 = factory.openSession().getMapper(UserMapper.class).selectByid(10);
System.out.println(user == user1);
}

(4)RowBounds 返回行范围必须相同
如果这样如下调用也可以命中缓存
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
Object user1 = sqlSession.selectOne("org.coderead.mybatis.UserMapper.selectByid", 10);
System.out.println(user == user1);
}

如果设置了分页,则不能命中一级缓存(RowBounds.DEFAULT时可以命中缓存)
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
//分页,返回0-10行
RowBounds rowBounds = new RowBounds(0,10);
List<Object> user1 = sqlSession.selectList(
"org.coderead.mybatis.UserMapper.selectByid", 10, rowBounds);
System.out.println(user == user1.get(0));
}

操作与配置相关
(1)未手清空才能命中缓存,否则不能命中缓存
@Test
public void test1(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
sqlSession.clearCache();
User user1 = mapper.selectByid(10);
System.out.println(user == user1);
}

注意,虽然编译是一次,但是查询查的是两次
(2)未调用 flushCache=true的查询才可能命中缓存
设置UserMapper中的selectByid的Options.FlushCachePolicy.TRUE,则不能命中一级缓存
@Select({"select * from users where id=#{1}"})
@Options(flushCache = Options.FlushCachePolicy.TRUE)
User selectByid(Integer id);

若,此时Mapper中如下所示
@Select({"select * from users where id=#{1}"})
@Options(flushCache = Options.FlushCachePolicy.TRUE)
User selectByid(Integer id);
@Select({"select * from users where id=#{1}"})
User selectByid3(Integer id);
则在执行mapper.selectByid(10);后同样会清空一级缓存
因为Options.FlushCachePolicy.TRUE = sqlSession.clearCache();
@Test
public void test1(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid3(10);
mapper.selectByid(10);
User user1 = mapper.selectByid3(10);
System.out.println(user == user1);
}

(3)未执行Update 才可能命中一级缓存
public void test1(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid3(10);
mapper.setName(10, "道友永存");//update
//数据一致性问题,相当于调用了 sqlSession.clearCache();
User user1 = mapper.selectByid3(10);
System.out.println(user == user1);
}

(4)缓存作用域不是 STATEMENT,STATEMENT表示关闭一级缓存(只是将作用域缩小至Statement,并不是真正关闭,若子查询引用了父类ID则还是可以命中一级缓存)
<settings>
<setting name="localCacheScope" value="STATEMENT"/>
</settings>
总结:

sqlSession.commit和sqlSession.rollback一样会去清空一级缓存,相当于clearCache()
一级缓存源码解析

运行时参数相关
测试一级缓存代码:
@Test
public void test3(){
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.selectByid(10);
User user1 = mapper.selectByid(10);
System.out.println(user == user1);
}


queryStack表示嵌套查询
在一级缓存中是以HashMap来存储的

查看CacheKey中保存的数据


第一次执行select,返回是null,去数据库查询queryFromDatabase

查询完成之后将结果保存至一级缓存


接着走第二次查询

此时可以在一级缓存中找到所缓存的数据,最后直接返回即可


操作与配置相关
只要执行了update就会清空一级缓存

如果配置了flushCache也会清空一级缓存(没有涉及到子查询)

query方法中,作用域如果是STATEMENT也会清空一级缓存

手动提交或回滚时也会清空一级缓存


总结:一级缓存和SqlSession相关,只要会话关闭,那么一级缓存全部清空

Spring继承 MyBatis 一级缓存失效违背了 与会话相关
如果集成时,没有配置事务的情况下每次都会构造新的会话
@Test
public void testBySpring(){
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
UserMapper mapper = context.getBean(UserMapper.class);
User user = mapper.selectByid(10);
User user1 =mapper.selectByid(10);
System.out.println(user == user1);
}

因为在mapper.selectByid(10);时,每次都会构造一个新的会话 发起调用,即代码中是两个不同的会话
验证:因为Executor和缓存是一比一的关系,所以如果两个select是相同的Executor,那么就表示是同一个SqlSession,也就是同一个缓存


可以发现是两个不同的Executor,不是同一级会话,所以不能命中缓存
只要加上事务,将两个查询放在一个事务中就可以命中一级缓存
@Test
public void testBySpring(){
ClassPathXmlApplicationContext context=new ClassPathXmlApplicationContext("spring.xml");
UserMapper mapper = context.getBean(UserMapper.class);
// 动态代理 动态代理 MyBatis
// mapper ->SqlSessionTemplate --> SqlSessionInterceptor-->SqlSessionFactory
DataSourceTransactionManager transactionManager =
(DataSourceTransactionManager) context.getBean("txManager");
// 手动开启事物
TransactionStatus status = transactionManager
.getTransaction(new DefaultTransactionDefinition());
User user = mapper.selectByid(10); // 每次都会构造一个新会话 发起调用
User user1 =mapper.selectByid(10);// 每次都会构造一个新会话 发起调用
System.out.println(user == user1);
}

对于Spring的context.getBean(UserMapper.class);是不同于MyBatis的Mapper的,Spring采用动态代理来执行
