深入理解MyBatis执行器的设计理念
文章目录
- 深入理解MyBatis执行器的设计理念
- JDBC中的statement类型
- 关于Statement简单执行器和PreparedStatement预处理执行器
- MyBatis的执行过程是怎样的?
- SqlSession
- Executor
- Executor的种类
JDBC中的statement类型
-
Statement(简单执行器)
作用:执行静态SQL,执行批处理、设置每次抓取的行数。
-
PreparedStatement(预处理执行器)
继承Statement接口,对SQL语句进行预编译,防止SQL注入。
-
CallableStatement(存储过程执行器)
继承PreparedStatement接口,用于调用数据库的存储过程,设置出参、读取出参。
注意:三个都是接口
关于Statement简单执行器和PreparedStatement预处理执行器
使用statement对象的优缺点
优点:可以一次性向数据库发送多条不同的SQL语句或者相同的SQL语句但参数不同,大大提高了效率。
缺点:一次传输的数据量相较于PreparedStatement的批处理要大
使用PreparedStatement对象的优缺点
优点:针对相同的SQL语句,只需要发送一次SQL语句,再发送一个参数组(包含着不同的参数)即可,数据量较小。
缺点:只适用于相同的SQL语句
MyBatis的执行过程是怎样的?
对于MyBatis框架,我们如果想要执行sql语句,一定是要先去建立一个SqlSession,我们先从这个SqlSession看起。
SqlSession
SqlSession是为Mybatis提供服务的主要Java接口,通过此接口可以执行命令,获得映射和管理事务。SqlSession并不是线程共享的,在一个SqlSession中可以执行多个sql,但多个SqlSession之间并不共享。
SqlSession为我们提供了各种各样的api,光是查询就提供了14个之多,这里SqlSession就采用了门面模式,为用户提供各种各样的api方便使用。
SqlSession的api可分为两类,基础api(增删改查),辅助api(提交、关闭会话),SqlSession的作用就像一个去饭店吃饭时的菜单,他并不负责做菜,他只是把所有的菜品都罗列出来让你选择,那么选好菜了下一步要干什么呢,往下看。
Executor
来到Executor中,相较于SqlSession并没有提供增和删,而是将其归类于修改,也就是说只有改、查。基础功能:改、查、缓存维护、事务维护,辅助API:提交、关闭执行器、批处理刷新(在执行批处理后一定要调用此方法,批处理才会生效,类似于事务的commit)。
Executor的种类
Executor可以分为下面几种类型,分别为SimpleExecutor简单执行器,ReuseExecutor可重用执行器,BatchExecutor批处理执行器,BaseExecutor,CachingExecutor二级缓存执行器。
下面分别演示一下三种执行器的区别
-
SimpleExecutor
//1.SimpleExecutor简单执行器 //select * from user where id=4 查询用户表中id为4的用户信息 @Test public void simpleTest() throws SQLException { SimpleExecutor executor = new SimpleExecutor(configuration,jdbcTransaction); List<Object> list = executor.doQuery(ms, 4, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(4)); executor.doQuery(ms, 4, RowBounds.DEFAULT, SimpleExecutor.NO_RESULT_HANDLER, ms.getBoundSql(4)); System.out.println(list.get(0)); }
查询结果:
==> Preparing: SELECT * FROM user WHERE id=?
> Parameters: 4(Integer)
< Total: 1
==> Preparing: SELECT * FROM user WHERE id=?
> Parameters: 4(Integer)
< Total: 1可以看到,同一条查询语句参数也相同,SimpleExecutor执行了两次,这个和我们上面讲的jdbc中的statement有点相似,他不会去重用sql,只会一条一条的执行。那我们再来看看可以重用sql的ReuseExecutor吧
-
ReuseExecutor
@Test public void reuseTest() throws SQLException { ReuseExecutor executor = new ReuseExecutor(configuration,jdbcTransaction); List<Object> list = executor.doQuery(ms, 4, RowBounds.DEFAULT, ReuseExecutor.NO_RESULT_HANDLER, ms.getBoundSql(4)); executor.doQuery(ms, 4, RowBounds.DEFAULT, ReuseExecutor.NO_RESULT_HANDLER, ms.getBoundSql(4)); System.out.println(list.get(0)); }
查询结果:
==> Preparing: SELECT * FROM user WHERE id=?
> Parameters: 4(Integer)
< Total: 1
> Parameters: 4(Integer)
< Total: 1这里就不一样了,通过这个ReuseExecutor执行器,我们可以发现,他只执行了一条sql,设置了两次参数。那他是怎么做到的呢,我们点进去看一下源码就知道了(为了便于阅读删除了部分方法)。
public class ReuseExecutor extends BaseExecutor { //内部维护了一个HashMap容器存放执行的sql语句 private final Map<String, Statement> statementMap = new HashMap<String, Statement>(); public ReuseExecutor(Configuration configuration, Transaction transaction) { super(configuration, transaction); } private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { Statement stmt; BoundSql boundSql = handler.getBoundSql(); String sql = boundSql.getSql(); //执行器前检查是否存在sql if (hasStatementFor(sql)) { stmt = getStatement(sql); applyTransactionTimeout(stmt); } else { //如果不存在,则创建出sql对应的statement,并放入map容器 Connection connection = getConnection(statementLog); stmt = handler.prepare(connection, transaction.getTimeout()); putStatement(sql, stmt); } handler.parameterize(stmt); return stmt; } //检查map容器中是否存在要执行的sql private boolean hasStatementFor(String sql) { try { return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed(); } catch (SQLException e) { return false; } } private Statement getStatement(String s) { return statementMap.get(s); } //将sql语句存入map容器 private void putStatement(String sql, Statement stmt) { statementMap.put(sql, stmt); } }
怎么样,是不是很简单。
-
BatchExecutor
@Test public void batchTest() throws SQLException { BatchExecutor executor = new BatchExecutor(configuration,jdbcTransaction); MappedStatement mappedStatement = configuration.getMappedStatement("com.lxy.dao.UserDao.update"); Map<String, Object> map = new HashMap<String, Object>(); map.put("name","张三"); map.put("id",4); executor.doUpdate(mappedStatement,map); executor.doUpdate(mappedStatement,map); executor.flushStatements(false); }
执行结果:
==> Preparing: update
user
setname
= ? where id = ?
==> Parameters: 张三(String), 4(Integer)
==> Parameters: 张三(String), 4(Integer)这里看起来是不是和ReuseExecutor很像,感觉他们好像就是一样的啊,有什么区别吗?是有区别的,ReuseExecutor是设置一次参数执行一次sql。而BatchExecutor如果设置多个参数,那么最新的参数会覆盖旧的参数,例如下面的例子。
@Test public void batchTest() throws SQLException { BatchExecutor executor = new BatchExecutor(configuration,jdbcTransaction); MappedStatement mappedStatement = configuration.getMappedStatement("com.lxy.dao.UserDao.update"); Map<String, Object> map = new HashMap<String, Object>(); map.put("name","张三"); map.put("id",4); Map<String, Object> map1 = new HashMap<String, Object>(); map1.put("name","张三2"); map1.put("id",4); executor.doUpdate(mappedStatement,map); executor.doUpdate(mappedStatement,map1); executor.flushStatements(false); }
我们稍作修改,多了一个map1,将map1中的name更改为张三2,执行结果如下:
==> Preparing: update
user
setname
= ? where id = ?
==> Parameters: 张三(String), 4(Integer)
==> Parameters: 张三2(String), 4(Integer)数据库结果:
注意:批处理执行器对查询语句是无效的,如果使用此执行器进行查询,效果和SimpleExecutor一样。
现在我们知道了批处理也会重用sql语句,但是下面演示的情况可就不一样了,在两次修改中穿插两次添加,最后的sql语句会有几条呢?
@Test public void batchTest(){ SqlSession session = factory.openSession(ExecutorType.BATCH,true); UserDao mapper = session.getMapper(UserDao.class); mapper.update("测试1",2); mapper.insert("批处理测试"); mapper.insert("批处理测试"); mapper.update("测试1",4); List<BatchResult> batchResults = session.flushStatements(); }
结果:
这里怎么不一样了,不是说批处理执行器也会重用sql语句吗,应该是两条啊,怎么变成三条了?
其实想让批处理执行器重用sql语句是有一定条件的,首先,调用的方法的sql和mappedStatement必须相同,其次,调用的顺序必须相邻,上面的演示并没有遵守第二个要求,所以对于修改语句,批处理执行器没有重用sql。
-
BaseExecutor
实现了Executor接口,是上面三个执行器的父类,它负责的是缓存的维护和事务管理。
-
CachingExecutor二级缓存执行器
这里体现的装饰器模式,装饰的是BaseExecutor,CachingExecutor内维护一个delegate,也就是BaseExecutor,通过执行delegate的方法前后进行一定的处理达到加强的效果。