架构
MyBatis分为4部分
- 动态代理(MapperProxy)
- SQL会话(SqlSession)
- 执行器(Executor)
- JDBC处理器(StatementHandler)
执行过程
总体架构采用门面模式来进行,也就是SqlSession只对外提供API(CRUD,提交和关闭会话),其内部本身并不知道如何去实现的,就像餐厅的服务员一样,只知道点菜,并不知道菜怎么制作,具体的实现逻辑是交由SqlSession里面的Executor构造器来实现的
下面来研究一下从SqlSession获取Mapper去执行的时候,发生了什么
首先来看一下官网上是怎么使用MyBatis的(不使用配置文件)
这样就可以调用运行了,下面就来分析一下从sqlsession开始去执行sql的过程(前面的过程其实就是设置配置文件,并且读取配置文件,以后再研究)
可以看到,这里SqlSessionFactory的实现是DefaultSessionFactory,所以看一下是怎么打开一个session的
调用的代码如下
public SqlSession openSession() {
return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
}
而openSessionFromDataSource的源码如下
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
DefaultSqlSession var8;
try {
//获取配置文件里面的环境信息(连接数据库那些信息)
Environment environment = this.configuration.getEnvironment();
//获取配置文件里面的TransactionFactory(事务工厂用来生成事务的)
TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
//录用事务工厂去创建一个新的事务
//(并且注意,这里的autoCommit是为false)
//所以是不会自动提交的
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
//根据配置去获取executor,也就是执行器(sql的执行都是由他来进行的)
//并且这里的执行器是this.configuration.getDefaultExecutorType()
//也就是默认的Executor
//而且注意,这个执行器是装配了新建的事务的
Executor executor = this.configuration.newExecutor(tx, execType);
//根据配置,executor和是否自动提交创建一个默认会话
var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
} catch (Exception var12) {
this.closeTransaction(tx);
throw ExceptionFactory.wrapException("Error opening session. Cause: " + var12, var12);
} finally {
ErrorContext.instance().reset();
}
return var8;
}
Executor
Executor负责进行CRUD,但其实本质上,Excutor里面只有改、查(原生的JDBC里面也只有改和查,即executeQuery和executeUpdate),同时也会涉及到一个重要的区域,也就是缓存,查会先经过缓存,如果缓存里面有,即直接取缓存,改也会经过缓存,所以Executor还要去维护缓存,当然还会有一些辅助功能,比如提交事务、关闭执行器和进行批处理
总的来说,Excutor的功能有以下几个
基本功能
- executeQuery
- executeUpdate
- 缓存维护
辅助功能
- 提交事务
- 关闭执行器
- 批处理刷新(可以理解成在执行多句增删改操作时,一开始只是在准备Sql,只要进行批处理刷新,这些Sql会被当成一个事务提交到MySQL处执行)
下面来看看下,ibatis的Executor有哪些实现类
BaseExcutor
这是一个抽象类,用来存放Executor共有功能的,比如获取连接、缓存操作(一级缓存)
那么具体是怎样的呢?
BaseExecutor有两个重要的方法
- query
- update
这两个方法里面都会调用交由实现类去实现的方法,分别是doQuery和doUpdate,所以在做完缓存之后,再进行实现类里面的doQuery和doUpdate方法,总的来说query和update方法的设置采用了模板设计模式
query方法
源码如下
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
//判断处理器关闭了没有
if (this.closed) {
//关闭仍然调用就去抛出异常
throw new ExecutorException("Executor was closed.");
} else {
//没关闭的话,判断需不需要清除一级缓存
//也就是localCache
//判断条件为:
//1.第一次查询一级缓存(queryStack是一个计数器,代表该事务经过了几次这个查询,一个executor只有一个事务)
//2.配置文件是否开启了清除一级缓存,如果开启了,进行清除(默认关闭)
//这两个条件要都为true才会清除,所以,一级缓存的清除就会受两个条件影响
//1.同一个会话执行多次sql
//2.配置文件的flushCacheRequire设置为false,或默认
if (this.queryStack == 0 && ms.isFlushCacheRequired()) {
this.clearLocalCache();
}
//下面就是查询的具体逻辑了
List list;
try {
//计数器自增
++this.queryStack;
//先尝试一级缓存中去获取结果
list = resultHandler == null ? (List)this.localCache.getObject(key) : null;
//如果不为Null,则代表一级缓存中是有存储的(存储的是结果!!!)
if (list != null) {
//如果是存储过程,更新本地缓存中存储过程的参数
this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果一级缓存中没有,就查找本地数据库
list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
//计数器又自减,变回原来值
--this.queryStack;
}
//下面这部分还不知道干什么的。。。
if (this.queryStack == 0) {
Iterator var8 = this.deferredLoads.iterator();
while(var8.hasNext()) {
BaseExecutor.DeferredLoad deferredLoad = (BaseExecutor.DeferredLoad)var8.next();
deferredLoad.load();
}
this.deferredLoads.clear();
if (this.configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
this.clearLocalCache();
}
}
return list;
}
}
可以看到key是怎么组成的
sessionID:方法全限定名:分页条件:sql:参数
queryFromDatabase方法
该方法就是当query方法尝试从一级缓存中取值时,如果一级缓存没有,就会走查询数据库
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
//首先在本地缓存这个sql,但此时本地缓存没该sql对应的结果
//这个动作仅仅代表有线程即将要把该key放入本地缓存
localCache.putObject(key, EXECUTION_PLACEHOLDER);
//下面就是进行真正的查询数据库
try {
//doQuery方法是由继承了BaseExecutor的方法去实现的
//从数据库中查找数据
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
//无论成功或失败,都要将一开始缓存的东西删掉
//虽然hashmap对于key值重复的会出现替代
//但如果失败了的话,本地不应该缓存这个key,是需要删除key值的!!!
localCache.removeObject(key);
}
//假如查找成功,真正往一级缓存中存放数据库查出来的结果
localCache.putObject(key, list);
//如果执行的是存储过程,则需要把参数也记录在本地缓存
if (ms.getStatementType() == StatementType.CALLABLE) {
localOutputParameterCache.putObject(key, parameter);
}
//返回结果
return list;
}
这里就有个疑问了?为什么还没有查出结果就存入本地缓存呢?这个动作有什么必要性吗??
SimpleExecutor(默认实现)
无论执行的Sql是什么,都要进行预编译,执行一句Sql就提交一次事务
预编译是由数据库去做的
doQuery方法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
//进行预编译
//与JDBC相似,获取statement然后去执行
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
//关闭资源
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
//获取连接
Connection connection = getConnection(statementLog);
//每次都会去预编译SQL
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
ReuseExecutor
相同的Sql不会进行预编译,执行一句Sql就提交一次事务
doQuery方法
可以看到其实doQuery方法与SimpleExecutor都挺类似的,但此时就是prepareStatement不同
@Override
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(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
Statement stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
}
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 {
//如果没有编译过
//获取连接再进行编译(预编译是由数据库去做的)
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
putStatement(sql, stmt);
}
handler.parameterize(stmt);
return stmt;
}
BatchExecutor
进行批处理执行器,执行多次操作后再进行提交事务,而且需要手动去提交事务(调用doUpdate时只是在提供Sql语句但还没放在MySql执行,需要进行doFlushStatements,也就是批处理刷新功能),只针对增删改操作,读操作跟SimpleExecutor一样,必须经过预编译
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
throws SQLException {
Statement stmt = null;
try {
//读取的时候,也会去批量执行剩余的statements
//(插入、修改才会存储起来statement,直到手动调用flushstatements才会清空)
//所以,假如在一堆update之后,突然来了一句query,那么前面的update都会被提交上去数据库执行
flushStatements();
Configuration configuration = ms.getConfiguration();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
//获取连接
Connection connection = getConnection(ms.getStatementLog());
//进行预编译,获取预编译的SQL
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
//执行
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
总结BaseExecutor
BaseExecutor总共有三种实现
- SimpleExecutor
- ReuseExecutor
- BatchExecutor
重点
- SimpleExecutor每次执行sql都会创建一个新的预处理器(PrepareStatement)
- ReuseExecutor当执行相同的Sql时,可以重用预处理器
- BatchExecutor是用来执行批量增删改处理的,批量去准备Sql语句,然后把批量Sql放在一个事务中进行提交,即必须执行flushStatements才会生效,而flushStatements方法是将BatchExecutor中存储的statements去执行,
缓存
前面已经提到过一级缓存是由BaseExecutor来实现的,那么二级缓存呢?
Caching Executor
Caching Executor同样实现了Executor接口,拥有二级缓存的功能,也有着Executor属性,而且使用了**装饰者模式(在不改变原有类结构和继承的情况下,通过组装这个类对象,来扩展一个新功能)**来调用一级缓存,也就是组装了BaseExecutor,也就是执行完二级缓存后,交给了下一个Executor
理解成就是,对BaseExecutor进行装饰,将它装进一个CachingExecutor中,添加了二级缓存功能,让BaseExecutor和CachingExecutor职责单一,BaseExecutor负责一级缓存,而CachingExecutor负责二级缓存,不破看原有的类结构,可以看到CachingExecutor装配上了Executor(以后如果看到delegate很有可能就是采用了装饰者模式,装饰者模式其实就是让类的关系不那么复杂,减少了一大堆继承关系)
二级缓存和一级缓存的区别
一级缓存是执行完语句就有,而二级缓存必须要进行事务提交才会有,因为二级缓存会进行跨线程的调用
执行缓存的顺序
先走二级缓存,再走一级缓存,二级缓存有了,就不会走一级缓存,具体逻辑在CachingExecutor的query方法里面
步骤如下
- 判断要不要走二级缓存
- 需要走,则进入二级缓存里找
- 返回Null,则代表找不到,继续交由组装的另一个Executor(delegate)去实现
- 不需要走,则交给CachingExecutor类里面组装的另一个Executor(delegate)去继续实现
- 需要走,则进入二级缓存里找
SqlSession调用过程
整体的执行过程,SqlSession去调用CachingExecutor,CachingExecutor通过装饰者模式,去组装BaseExecutor的其中一个实现类,BaseExecutor是有一级缓存和获取连接操作的 ,而BaseExecutor的实现类是有预处理功能的(PrepareStatement),CachingExecutor执行了二级缓存之后,调用组装的BaseExecutor,从而实现了职责单一
会话的作用其实就是降低调用复杂性,也就是使用门面模式(服务员点菜容易,还是厨师点菜容易?)
下面来看看,使用二级缓存是怎样运行的
CachingExecutor的query方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
//获取二级缓存
Cache cache = ms.getCache();
//如果二级缓存不为空
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
//从二级缓存中取结果
List<E> list = (List<E>) tcm.getObject(cache, key);
//如果没有,再调用BaseExectutor去经过一级缓存去拿结果
if (list == null) {
//调用BaseExecutor去取结果()
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
//将查询出来的结果放入二级缓存中去
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
//如果二级缓存为空,调用一级缓存的BaseExecutor的query方法(门面模式)
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
步骤如下
- 如果二级缓存开启了
- 从二级缓存中去取,如果没有,调用BaseExecutor去取结果(一级缓存或者访问数据库)
- 然后将结果存储进二级缓存
- 如果二级缓存没开启
- 调用BaseExecutor去获取结果(一级缓存或者访问数据库)