mybatis一个方法执行多条sql_精尽MyBatis源码分析——SQL执行过程之Executor!
MyBatis的SQL执行过程
在前面一系列的文档中,我已经分析了 MyBatis 的基础支持层以及整个的初始化过程,此时 MyBatis 已经处于就绪状态了,等待使用者发号施令了
那么接下来我们来看看它执行SQL的整个过程,该过程比较复杂,涉及到二级缓存,将返回结果转换成 Java 对象以及延迟加载等等处理过程,这里将一步一步地进行分析:
- 《SQL执行过程(一)之Executor》
- 《SQL执行过程(二)之StatementHandler》
- 《SQL执行过程(三)之ResultSetHandler》
- 《SQL执行过程(四)之延迟加载》
MyBatis中SQL执行的整体过程如下图所示:
在 SqlSession 中,会将执行 SQL 的过程交由Executor
执行器去执行,过程大致如下:
- 通过
DefaultSqlSessionFactory
创建与数据库交互的SqlSession
“会话”,其内部会创建一个Executor
执行器对象 - 然后
Executor
执行器通过StatementHandler
创建对应的java.sql.Statement
对象,并通过ParameterHandler
设置参数,然后执行数据库相关操作 - 如果是数据库更新操作,则可能需要通过
KeyGenerator
先设置自增键,然后返回受影响的行数 - 如果是数据库查询操作,则需要将数据库返回的
ResultSet
结果集对象包装成ResultSetWrapper
,然后通过DefaultResultSetHandler
对结果集进行映射,最后返回 Java 对象
上面还涉及到一级缓存、二级缓存和延迟加载等其他处理过程
SQL执行过程(一)之Executor
在MyBatis的SQL执行过程中,Executor执行器担当着一个重要的角色,相关操作都需要通过它来执行,相当于一个调度器,把SQL语句交给它,它来调用各个组件执行操作
其中一级缓存和二级缓存都是在Executor执行器中完成的
Executor
执行器接口的实现类如下图所示:
org.apache.ibatis.executor.BaseExecutor
:实现Executor接口,提供骨架方法,支持一级缓存,指定几个抽象的方法交由不同的子类去实现org.apache.ibatis.executor.SimpleExecutor
:继承 BaseExecutor 抽象类,简单的 Executor 实现类(默认)org.apache.ibatis.executor.ReuseExecutor
:继承 BaseExecutor 抽象类,可重用的 Executor 实现类,相比SimpleExecutor,在Statement执行完操作后不会立即关闭,而是缓存起来,执行的SQL作为key,下次执行相同的SQL时优先从缓存中获取Statement对象org.apache.ibatis.executor.BatchExecutor
:继承 BaseExecutor 抽象类,支持批量执行的 Executor 实现类org.apache.ibatis.executor.CachingExecutor
:实现 Executor 接口,支持二级缓存的 Executor 的实现类,实际采用了装饰器模式,装饰对象为左边三个Executor类
Executor
org.apache.ibatis.executor.Executor
:执行器接口,代码如下:
public interface Executor {/*** ResultHandler 空对象*/ResultHandler NO_RESULT_HANDLER = null;/*** 更新或者插入或者删除* 由传入的 MappedStatement 的 SQL 所决定*/int update(MappedStatement ms, Object parameter) throws SQLException;/*** 查询,带 ResultHandler + CacheKey + BoundSql*/<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,CacheKey cacheKey, BoundSql boundSql) throws SQLException;/*** 查询,带 ResultHandler*/<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)throws SQLException;/*** 查询,返回 Cursor 游标*/<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;/*** 刷入批处理语句*/List<BatchResult> flushStatements() throws SQLException;/*** 提交事务*/void commit(boolean required) throws SQLException;/*** 回滚事务*/void rollback(boolean required) throws SQLException;/*** 创建 CacheKey 对象*/CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);/*** 判断是否缓存*/boolean isCached(MappedStatement ms, CacheKey key);/*** 清除本地缓存*/void clearLocalCache();/*** 延迟加载*/void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);/*** 获得事务*/Transaction getTransaction();/*** 关闭事务*/void close(boolean forceRollback);/*** 判断事务是否关闭*/boolean isClosed();/*** 设置包装的 Executor 对象*/void setExecutorWrapper(Executor executor);
}
执行器接口定义了操作数据库的相关方法:
- 数据库的读和写操作
- 事务相关
- 缓存相关
- 设置延迟加载
- 设置包装的 Executor 对象
BaseExecutor
org.apache.ibatis.executor.BaseExecutor
:实现Executor接口,提供骨架方法,指定几个抽象的方法交由不同的子类去实现,例如:
protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException;protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds,BoundSql boundSql) throws SQLException;
上面这四个方法交由不同的子类去实现,分别是:更新数据库、刷入批处理语句、查询数据库和查询数据返回游标
构造方法
public abstract class BaseExecutor implements Executor {private static final Log log = LogFactory.getLog(BaseExecutor.class);/*** 事务对象*/protected Transaction transaction;/*** 包装的 Executor 对象*/protected Executor wrapper;/*** DeferredLoad(延迟加载)队列*/protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;/*** 本地缓存,即一级缓存,内部就是一个 HashMap 对象*/protected PerpetualCache localCache;/*** 本地输出类型参数的缓存,和存储过程有关*/protected PerpetualCache localOutputParameterCache;/*** 全局配置*/protected Configuration configuration;/*** 记录当前会话正在查询的数量*/protected int queryStack;/*** 是否关闭*/private boolean closed;protected BaseExecutor(Configuration configuration, Transaction transaction) {this.transaction = transaction;this.deferredLoads = new ConcurrentLinkedQueue<>();this.localCache = new PerpetualCache("LocalCache");this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");this.closed = false;this.configuration = configuration;this.wrapper = this;}
}
其中上面的属性可根据注释进行查看
这里提一下localCache
属性,本地缓存,用于一级缓存,MyBatis的一级缓存是什么呢?
每当我们使用 MyBatis 开启一次和数据库的会话,MyBatis 都会创建出一个 SqlSession 对象,表示与数据库的一次会话,而每个 SqlSession 都会创建一个 Executor 对象
在对数据库的一次会话中,我们有可能会反复地执行完全相同的查询语句,每一次查询都会访问一次数据库,如果在极短的时间内做了完全相同的查询,那么它们的结果极有可能完全相同,由于查询一次数据库的代价很大,如果不采取一些措施的话,可能造成很大的资源浪费
为了解决这一问题,减少资源的浪费,MyBatis 会在每一次 SqlSession 会话对象中建立一个简单的缓存,将每次查询到的结果缓存起来,当下次查询的时候,如果之前已有完全一样的查询,则会先尝试从这个简单的缓存中获取结果返回给用户,不需要再进行一次数据库查询了 注意,这个“简单的缓存”就是一级缓存,且默认开启,无法“关闭”
如下图所示,MyBatis 的一次会话:在一个 SqlSession 会话对象中创建一个localCache
本地缓存,对于每一次查询,都会根据查询条件尝试去localCache
本地缓存中获取缓存数据,如果存在,就直接从缓存中取出数据然后返回给用户,否则访问数据库进行查询,将查询结果存入缓存并返回给用户(如果设置的缓存区域为STATEMENT,默认为SESSION,在一次会话中所有查询执行后会清空当前 SqlSession 会话中的localCache
本地缓存,相当于“关闭”了一级缓存)<br/>所有的数据库更新操作都会清空当前 SqlSession 会话中的本地缓存
如上描述,MyBatis的一级缓存在多个 SqlSession 会话时,可能导致数据的不一致性,某一个 SqlSession 更新了数据而其他 SqlSession 无法获取到更新后的数据,出现数据不一致性,这种情况是不允许出现了,所以我们通常选择“关闭”一级缓存
clearLocalCache方法
clearLocalCache()
方法,清空一级(本地)缓存,如果全局配置中设置的localCacheScope
缓存区域为STATEMENT
(默认为SESSION
),则在每一次查询后会调用该方法,相当于关闭了一级缓存,代码如下:
@Override
public void clearLocalCache() {if (!closed) {localCache.clear();localOutputParameterCache.clear();}
}
createCacheKey方法
createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql)
方法,根据本地查询的相关信息创建一个CacheKey
缓存key对象,代码如下:
@Override
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {if (closed) {throw new ExecutorException("Executor was closed.");}// <1> 创建 CacheKey 对象CacheKey cacheKey = new CacheKey();// <2> 设置 id、offset、limit、sql 到 CacheKey 对象中cacheKey.update(ms.getId());cacheKey.update(rowBounds.getOffset());cacheKey.update(rowBounds.getLimit());cacheKey.update(boundSql.getSql());// <3> 设置 ParameterMapping 数组的元素对应的每个 value 到 CacheKey 对象中List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();// mimic DefaultParameterHandler logicfor (ParameterMapping parameterMapping : parameterMappings) {if (parameterMapping.getMode() != ParameterMode.OUT) { // 该参数需要作为入参Object value;String propertyName = parameterMapping.getProperty();/** 获取该属性值*/if (boundSql.hasAdditionalParameter(propertyName)) {// 从附加参数中获取value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {// 入参对象为空则直接返回 nullvalue = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 入参有对应的类型处理器则直接返回该参数value = parameterObject;} else {// 从入参对象中获取该属性的值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}cacheKey.update(value);}}// <4> 设置 Environment.id 到 CacheKey 对象中if (configuration.getEnvironment() != null) {// issue #176cacheKey.update(configuration.getEnvironment().getId());}return cacheKey;
}
- 创建一个
CacheKey
实例对象 - 将入参中的
id
、offset
、limit
、sql
,通过CacheKey
的update
方法添加到其中,它的方法如下:
public void update(Object object) {// 方法参数 object 的 hashcodeint baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);this.count++;// checksum 为 baseHashCode 的求和this.checksum += baseHashCode;// 计算新的 hashcode 值baseHashCode *= this.count;this.hashcode = this.multiplier * this.hashcode + baseHashCode;// 添加 object 到 updateList 中this.updateList.add(object);
}
- 获取本次查询的入参值,通过
CacheKey
的update
方法添加到其中 - 获取本次环境的
Environment.id
,通过CacheKey
的update
方法添加到其中 - 返回
CacheKey
实例对象,这样就可以为本次查询生成一个唯一的缓存key对象,可以看看CacheKey
重写的equal
方法:
@Override
public boolean equals(Object object) {if (this == object) {return true;}if (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey = (CacheKey) object;if (hashcode != cacheKey.hashcode) {return false;}if (checksum != cacheKey.checksum) {return false;}if (count != cacheKey.count) {return false;}for (int i = 0; i < updateList.size(); i++) {Object thisObject = updateList.get(i);Object thatObject = cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;
}
query相关方法
查询数据库因为涉及到一级缓存,所以这里有多层方法,最终访问数据库的doQuery
方法是交由子类去实现的,总共分为三层:
- 根据入参获取BoundSql和CacheKey对象,然后再去调用查询方法
- 涉及到一级缓存和延迟加载的处理,缓存未命中则再去调用查询数据库的方法
- 保存一些信息供一级缓存使用,内部调用
doQuery
方法执行数据库的读操作
接下来我们分别来看看这三个方法
① query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
方法,数据库查询操作的入口,代码如下
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)throws SQLException {// <1> 获得 BoundSql 对象BoundSql boundSql = ms.getBoundSql(parameter);// <2> 创建 CacheKey 对象CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);// <3> 查询return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
- 通过
MappedStatement
对象根据入参获取BoundSql
对象,如果是动态SQL则需要进行解析,获取到最终的SQL,替换成?
占位符 - 调用
createCacheKey
方法为本次查询创建一个CacheKey
对象 - 继续调用
query(...)
方法执行查询
② query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
方法,处理数据库查询操作,涉及到一级缓存,代码如下:
@Override
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());// <1> 已经关闭,则抛出 ExecutorException 异常if (closed) {throw new ExecutorException("Executor was closed.");}// <2> 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存(配置了 flushCache = true)if (queryStack == 0 && ms.isFlushCacheRequired()) {clearLocalCache();}List<E> list;try {// <3> queryStack + 1queryStack++;// <4> 从一级缓存中,获取查询结果list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;if (list != null) { // <4.1> 获取到,则进行处理// 处理缓存存储过程的结果handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else { // <4.2> 获得不到,则从数据库中查询list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {// <5> queryStack - 1queryStack--;}if (queryStack == 0) { // <6> 如果当前会话的所有查询执行完了// <6.1> 执行延迟加载for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601// <6.2> 清空 deferredLoadsdeferredLoads.clear();// <6.3> 如果缓存级别是 LocalCacheScope.STATEMENT ,则进行清理if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}// <7> 返回查询结果return list;
}
- 当前会话已经被关闭则抛出异常
- 如果
queryStack
为0
(表示是当前会话只有本次查询而没有其他的查询了),并且要求清空本地缓存(配置了flushCache=true
),那么直接清空一级(本地)缓存 - 当前会话正在查询的数量加一,
queryStack++
- 从
localCache
一级缓存获取缓存的查询结果- 如果有缓存数据,则需要处理储存过程的情况,将需要作为出参(
OUT
)的参数设置到本次查询的入参的属性中 - 如果没有缓存数据,则调用
queryFromDatabase
方法,执行数据库查询操作
- 如果有缓存数据,则需要处理储存过程的情况,将需要作为出参(
- 当前会话正在查询的数量减一,
queryStack--
- 如果当前会话所有查询都执行完
- 执行当前会话中的所有的延迟加载
deferredLoads
,这种延迟加载属于查询后的延迟,和后续讲到的获取属性时再加载不同,这里的延迟加载是在哪里生成的呢?
在DefaultResultSetHandler
中进行结果映射时,如果某个属性配置的是子查询,并且本次的子查询在一级缓存中有缓存数据,那么将会创建一个DeferredLoad
对象保存在deferredLoads
中,该属性值先设置为DEFERRED
延迟加载对象(final修饰的Object对象),待当前会话所有的查询结束后,也就是当前执行步骤,则会从一级缓存获取到数据设置到返回结果中 - 清空所有的延迟加载
deferredLoads
对象 - 如果全局配置的缓存级别为STATEMENT(默认为SESSION),则清空当前会话中一级缓存的所有数据
- 执行当前会话中的所有的延迟加载
- 返回查询结果
③ queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
方法,执行数据库查询操作,代码如下:
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds,ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List<E> list;// <1> 在缓存中,添加正在执行的占位符对象,因为正在执行的查询不允许提前加载需要延迟加载的属性,可见 DeferredLoad#canLoad() 方法localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// <2> 执行读操作list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// <3> 从缓存中,移除占位对象localCache.removeObject(key);}// <4> 添加到缓存中localCache.putObject(key, list);// <5> 如果是存储过程,则将入参信息保存保存,跟一级缓存处理存储过程相关if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}// <6> 返回查询结果return list;
}
- 在缓存中,添加正在执行的
EXECUTION_PLACEHOLDER
占位符对象,因为正在执行的查询不允许提前加载需要延迟加载的属性,可见 DeferredLoad#canLoad() 方法 - 调用查询数据库
doQuery
方法,该方法交由子类实现 - 删除第
1
步添加的占位符 - 将查询结果添加到
localCache
一级缓存中 - 如果是存储过程,则将入参信息保存保存,跟一级缓存处理存储过程相关,可见上面的第
②
个方法的第4.1
步 - 返回查询结果
update方法
update(MappedStatement ms, Object parameter)
方法,执行更新数据库的操作,代码如下:
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());// <1> 已经关闭,则抛出 ExecutorException 异常if (closed) {throw new ExecutorException("Executor was closed.");}// <2> 清空本地缓存clearLocalCache();// <3> 执行写操作return doUpdate(ms, parameter);
}
- 当前会话已经被关闭则抛出异常
- 清空当前会话中一级缓存的所有数据
- 调用更新数据库
doUpdate
方法,该方法交由子类实现
其他方法
除了上面介绍的几个重要的方法以外,还有其他很多方法,例如获取当前事务,提交事务,回滚事务,关闭会话等等,这里我就不一一列出来了,请自行阅读该类
SimpleExecutor
org.apache.ibatis.executor.SimpleExecutor
:继承 BaseExecutor 抽象类,简单的 Executor 实现类(默认使用)
- 每次对数据库的操作,都会创建对应的Statement对象
- 执行完成后,关闭该Statement对象
代码如下:
public class SimpleExecutor extends BaseExecutor {public SimpleExecutor(Configuration configuration, Transaction transaction) {super(configuration, transaction);}@Overridepublic int doUpdate(MappedStatement ms, Object parameter) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 创建 StatementHandler 对象StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);// 初始化 Statement 对象stmt = prepareStatement(handler, ms.getStatementLog());// 通过 StatementHandler 执行写操作return handler.update(stmt);} finally {// 关闭 Statement 对象closeStatement(stmt);}}@Overridepublic <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 对象StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 初始化 Statement 对象stmt = prepareStatement(handler, ms.getStatementLog());// 通过 StatementHandler 执行读操作return handler.query(stmt, resultHandler);} finally {// 关闭 Statement 对象closeStatement(stmt);}}@Overrideprotected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)throws SQLException {Configuration configuration = ms.getConfiguration();StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);Statement stmt = prepareStatement(handler, ms.getStatementLog());Cursor<E> cursor = handler.queryCursor(stmt);stmt.closeOnCompletion();return cursor;}@Overridepublic List<BatchResult> doFlushStatements(boolean isRollback) {return Collections.emptyList();}private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;// 获得 Connection 对象,如果开启了 Debug 模式,则返回的是一个代理对象Connection connection = getConnection(statementLog);// 创建 Statement 或 PrepareStatement 对象stmt = handler.prepare(connection, transaction.getTimeout());// 往 Statement 中设置 SQL 语句上的参数,例如 PrepareStatement 的 ? 占位符handler.parameterize(stmt);return stmt;}
}
我们看到这些方法的实现,其中的步骤差不多都是一样的
- 获取
Configuration
全局配置对象 - 通过上面全局配置对象的
newStatementHandler
方法,创建RoutingStatementHandler
对象,采用了装饰器模式,根据配置的StatementType
创建对应的对象,默认为PreparedStatementHandler
对象,进入BaseStatementHandler
的构造方法你会发现有几个重要的步骤,在后续会讲到
然后使用插件链对该对象进行应用,方法如下所示
// Configuration.java
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {/** 创建 RoutingStatementHandler 路由对象* 其中根据 StatementType 创建对应类型的 Statement 对象,默认为 PREPARED* 执行的方法都会路由到该对象*/StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 将 Configuration 全局配置中的所有插件应用在 StatementHandler 上面statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;
}
- 调用
prepareStatement
方法初始化Statement
对象- 从事务中获取一个
Connection
数据库连接,如果开启了Debug模式,则会为该Connection
创建一个动态代理对象的实例,用于打印Debug日志 - 通过上面第
2
步创建的StatementHandler
对象创建一个Statement
对象(默认为PrepareStatement
),还会进行一些准备工作,例如:如果配置了KeyGenerator
(设置主键),则会设置需要返回相应自增键,在后续会讲到 - 往
Statement
对象中设置SQL的参数,例如PrepareStatement
的?
占位符,实际上是通过DefaultParameterHandler
设置占位符参数 - 返回已经创建好的
Statement
对象,就等待着执行数据库操作了
- 从事务中获取一个
- 通过
StatementHandler
对Statement
进行数据库的操作,如果是查询操作则会通过DefaultResultSetHandler
进行参数映射(非常复杂,后续逐步分析 )
ReuseExecutor
org.apache.ibatis.executor.ReuseExecutor
:继承 BaseExecutor 抽象类,可重用的 Executor 实现类
- 每次对数据库的操作,优先从当前会话的缓存中获取对应的Statement对象,如果不存在,才进行创建,创建好了会放入缓存中
- 数据库操作执行完成后,不关闭该Statement对象
- 其它的和SimpleExecutor是一致的
我们来看看他的prepareStatement
方法就好了:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql();/** 根据需要执行的 SQL 语句判断 是否已有对应的 Statement 并且连接未关闭*/if (hasStatementFor(sql)) {// 从缓存中获得 Statement 对象stmt = getStatement(sql);// 重新设置事务超时时间applyTransactionTimeout(stmt);} else {// 获得 Connection 对象Connection connection = getConnection(statementLog);// 初始化 Statement 对象stmt = handler.prepare(connection, transaction.getTimeout());// 将 Statement 添加到缓存中,key 值为 当前执行的 SQL 语句putStatement(sql, stmt);}// 往 Statement 中设置 SQL 语句上的参数,例如 PrepareStatement 的 ? 占位符handler.parameterize(stmt);return stmt;
}
在创建Statement
对象前,会根据本次查询的SQL从本地的Map<String, Statement> statementMap
获取到对应的Statement
对象
- 如果缓存命中,并且该对象的连接未关闭,那么重新设置当前事务的超时时间
- 如果缓存未命中,则执行和
SimpleExecutor
中的prepareStatement
方法相同逻辑创建一个Statement
对象并放入statementMap
缓存中
BatchExecutor
org.apache.ibatis.executor.BatchExecutor
:继承 BaseExecutor 抽象类,支持批量执行的 Executor 实现类
- 我们在执行数据库的更新操作时,可以通过
Statement
的addBatch()
方法将数据库操作添加到批处理中,等待调用Statement
的executeBatch()
方法进行批处理 BatchExecutor
维护了多个Statement
对象,一个对象对应一个SQL(sql
和MappedStatement
对象都相等),每个Statement
对象对应多个数据库操作(同一个sql
多种入参),就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库
由于JDBC不支持数据库查询的批处理,所以这里就不展示它数据库查询的实现方法,和SimpleExecutor一致,我们来看看其他的方法
构造方法
public class BatchExecutor extends BaseExecutor {public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;/*** Statement 数组*/private final List<Statement> statementList = new ArrayList<>();/*** BatchResult 数组** 每一个 BatchResult 元素,对应 {@link #statementList} 集合中的一个 Statement 元素*/private final List<BatchResult> batchResultList = new ArrayList<>();/*** 上一次添加至批处理的 Statement 对象对应的SQL*/private String currentSql;/*** 上一次添加至批处理的 Statement 对象对应的 MappedStatement 对象*/private MappedStatement currentStatement;public BatchExecutor(Configuration configuration, Transaction transaction) {super(configuration, transaction);}
}
statementList
属性:维护多个Statement
对象batchResultList
属性:维护多个BatchResult
对象,每个对象对应上面的一个Statement
对象,每个BatchResult
对象包含同一个SQL和其每一次操作的入参currentSql
属性:上一次添加至批处理的Statement
对象对应的SQLcurrentStatement
属性:上一次添加至批处理的Statement
对象对应的MappedStatement
对象
BatchResult
org.apache.ibatis.executor.BatchResult
:相同SQL(sql
和MappedStatement
对象都相等)聚合的结果,包含了同一个SQL每一次操作的入参,代码如下:
public class BatchResult {/*** MappedStatement 对象*/private final MappedStatement mappedStatement;/*** SQL*/private final String sql;/*** 参数对象集合** 每一个元素,对应一次操作的参数*/private final List<Object> parameterObjects;/*** 更新数量集合** 每一个元素,对应一次操作的更新数量*/private int[] updateCounts;public BatchResult(MappedStatement mappedStatement, String sql) {super();this.mappedStatement = mappedStatement;this.sql = sql;this.parameterObjects = new ArrayList<>();}public BatchResult(MappedStatement mappedStatement, String sql, Object parameterObject) {this(mappedStatement, sql);addParameterObject(parameterObject);}public void addParameterObject(Object parameterObject) {this.parameterObjects.add(parameterObject);}
}
doUpdate方法
更新数据库的操作,添加至批处理,需要调用doFlushStatements
执行批处理,代码如下:
@Override
public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {final Configuration configuration = ms.getConfiguration();// <1> 创建 StatementHandler 对象final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);final BoundSql boundSql = handler.getBoundSql();final String sql = boundSql.getSql();final Statement stmt;// <2> 如果和上一次添加至批处理 Statement 对象对应的 currentSql 和 currentStatement 都一致,则聚合到 BatchResult 中if (sql.equals(currentSql) && ms.equals(currentStatement)) {// <2.1> 获取上一次添加至批处理 Statement 对象int last = statementList.size() - 1;stmt = statementList.get(last);// <2.2> 重新设置事务超时时间applyTransactionTimeout(stmt);// <2.3> 往 Statement 中设置 SQL 语句上的参数,例如 PrepareStatement 的 ? 占位符handler.parameterize(stmt);// fix Issues 322// <2.4> 获取上一次添加至批处理 Statement 对应的 BatchResult 对象,将本次的入参添加到其中BatchResult batchResult = batchResultList.get(last);batchResult.addParameterObject(parameterObject);} else { // <3> 否则,创建 Statement 和 BatchResult 对象// <3.1> 初始化 Statement 对象Connection connection = getConnection(ms.getStatementLog());stmt = handler.prepare(connection, transaction.getTimeout());handler.parameterize(stmt); // fix Issues 322// <3.2> 设置 currentSql 和 currentStatemencurrentSql = sql;currentStatement = ms;// <3.3> 添加 Statement 到 statementList 中statementList.add(stmt);// <3.4> 创建 BatchResult 对象,并添加到 batchResultList 中batchResultList.add(new BatchResult(ms, sql, parameterObject));}// <4> 添加至批处理handler.batch(stmt);// <5> 返回 Integer.MIN_VALUE + 1002return BATCH_UPDATE_RETURN_VALUE;
}
- 创建
StatementHandler
对象,和SimpleExecutor
中一致,在后续会讲到 - 如果和上一次添加至批处理
Statement
对象对应的currentSql
和currentStatement
都一致,则聚合到BatchResult
中- 获取上一次添加至批处理
Statement
对象 - 重新设置事务超时时间
- 往
Statement
中设置 SQL 语句上的参数,例如PrepareStatement
的?
占位符,在SimpleExecutor
中已经讲到 - 获取上一次添加至批处理
Statement
对应的BatchResult
对象,将本次的入参添加到其中
- 获取上一次添加至批处理
- 否则,创建
Statement
和BatchResult
对象- 初始化
Statement
对象,在SimpleExecutor
中已经讲到,这里就不再重复了 - 设置
currentSql
和currentStatemen
属性 - 添加
Statement
到statementList
集合中 - 创建
BatchResult
对象,并添加到batchResultList
集合中
- 初始化
- 添加至批处理
- 返回
Integer.MIN_VALUE + 1002
,为什么返回这个值?不清楚
doFlushStatements方法
执行批处理,也就是将之前添加至批处理的数据库更新操作进行批处理,代码如下:
@Override
public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {try {List<BatchResult> results = new ArrayList<>();if (isRollback) { // <1> 如果 isRollback 为 true ,返回空数组return Collections.emptyList();}// <2> 遍历 statementList 和 batchResultList 数组,逐个提交批处理for (int i = 0, n = statementList.size(); i < n; i++) {// <2.1> 获得 Statement 和 BatchResult 对象Statement stmt = statementList.get(i);applyTransactionTimeout(stmt);BatchResult batchResult = batchResultList.get(i);try {// <2.2> 提交该 Statement 的批处理batchResult.setUpdateCounts(stmt.executeBatch());MappedStatement ms = batchResult.getMappedStatement();List<Object> parameterObjects = batchResult.getParameterObjects();/** <2.3> 获得 KeyGenerator 对象* 1. 配置了 <selectKey /> 则会生成 SelectKeyGenerator 对象* 2. 配置了 useGeneratedKeys="true" 则会生成 Jdbc3KeyGenerator 对象* 否则为 NoKeyGenerator 对象*/KeyGenerator keyGenerator = ms.getKeyGenerator();if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;// <2.3.1> 批处理入参对象集合,设置自增键jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);} else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { // issue #141for (Object parameter : parameterObjects) {// <2.3.1> 一次处理每个入参对象,设置自增键keyGenerator.processAfter(this, ms, stmt, parameter);}}// Close statement to close cursor #1109// <2.4> 关闭 Statement 对象closeStatement(stmt);} catch (BatchUpdateException e) {// 如果发生异常,则抛出 BatchExecutorException 异常StringBuilder message = new StringBuilder();message.append(batchResult.getMappedStatement().getId()).append(" (batch index #").append(i + 1).append(")").append(" failed.");if (i > 0) {message.append(" ").append(i).append(" prior sub executor(s) completed successfully, but will be rolled back.");}throw new BatchExecutorException(message.toString(), e, results, batchResult);}// <2.5> 添加到结果集results.add(batchResult);}return results;} finally {// <3.1> 关闭 Statement 们for (Statement stmt : statementList) {closeStatement(stmt);}// <3.2> 置空 currentSql、statementList、batchResultList 属性currentSql = null;statementList.clear();batchResultList.clear();}
}
在调用doUpdate方法将数据库更新操作添加至批处理后,我们需要调用doFlushStatements方法执行批处理,逻辑如下:
- 如果
isRollback
为true
,表示需要回退,返回空数组 - 遍历
statementList
和batchResultList
数组,逐个提交批处理- 获得
Statement
和BatchResult
对象 - 提交该
Statement
的批处理 - 获得
KeyGenerator
对象,用于设置自增键,在后续会讲到 - 关闭
Statement
对象 - 将
BatchResult
对象添加到结果集
- 获得
- 最后会关闭所有的
Statement
和清空当前会话中保存的数据
二级缓存
在BaseExecutor
中讲到的一级缓存中,缓存数据仅在当前的 SqlSession 会话中进行共享,可能会导致多个 SqlSession 出现数据不一致性的问题
如果需要在多个 SqlSession 之间需要共享缓存数据,则需要使用到二级缓存
开启二级缓存
后,会使用CachingExecutor
对象装饰其他的Executor
类,这样会先在CachingExecutor
进行二级缓存
的查询,缓存未命中则进入装饰的对象中,进行一级缓存
的查询
流程如下图所示:
MappedStatement会有一个Cache对象,是根据@CacheNamespace
注解或<cache />
标签创建的对象,该对象也会保存在Configuration
全局配置对象的Map<String, Cache> caches = new StrictMap<>("Caches collection")
中,key为所在的namespace,也可以通过@CacheNamespaceRef
注解或<cache-ref />
标签来指定其他namespace的Cache对象
在全局配置对象中cacheEnabled
是否开启缓存属性默认为true
,可以在mybatis-config.xml
配置文件中添加以下配置关闭:
<configuration><settings><setting name="cacheEnabled" value="false" /></settings>
</configuration>
我们来看看MyBatis是如何实现二级缓存的
CachingExecutor
org.apache.ibatis.executor.CachingExecutor
:实现 Executor 接口,支持二级缓存的 Executor 的实现类
构造方法
public class CachingExecutor implements Executor {/*** 被委托的 Executor 对象*/private final Executor delegate;/*** TransactionalCacheManager 对象*/private final TransactionalCacheManager tcm = new TransactionalCacheManager();public CachingExecutor(Executor delegate) {this.delegate = delegate;// 设置 delegate 被当前执行器所包装delegate.setExecutorWrapper(this);}
}
delegate
属性,为被委托的Executor
对象,具体的数据库操作都是交由它去执行tcm
属性,TransactionalCacheManager
对象,支持事务的缓存管理器,因为二级缓存是支持跨 SqlSession 共享的,此处需要考虑事务,那么,必然需要做到事务提交时,才将当前事务中查询时产生的缓存,同步到二级缓存中,所以需要通过TransactionalCacheManager
来实现
query方法
处理数据库查询操作的方法,涉及到二级缓存,会将Cache
二级缓存对象装饰成TransactionalCache
对象并存放在TransactionalCacheManager
管理器中,代码如下:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds,ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// <1> 获取 Cache 二级缓存对象Cache cache = ms.getCache();// <2> 如果配置了二级缓存if (cache != null) {// <2.1> 如果需要清空缓存,则进行清空flushCacheIfRequired(ms);// <2.2> 如果当前操作需要使用缓存(默认开启)if (ms.isUseCache() && resultHandler == null) {// <2.2.1> 如果是存储过程相关操作,保证所有的参数模式为 ParameterMode.INensureNoOutParams(ms, boundSql);// <2.2.2> 从二级缓存中获取结果,会装饰成 TransactionalCacheList<E> list = (List<E>) tcm.getObject(cache, key);if (list == null) {// <2.2.3> 如果不存在,则从数据库中查询list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// <2.2.4> 将缓存结果保存至 TransactionalCachetcm.putObject(cache, key, list); // issue #578 and #116}// <2.2.5> 直接返回结果return list;}}// <3> 没有使用二级缓存,则调用委托对象的方法return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
- 获取
Cache
二级缓存对象 - 如果该对象不为空,表示配置了二级缓存
- 如果需要清空缓存,则进行清空
- 如果当前操作需要使用缓存(默认开启)
- 如果是存储过程相关操作,保证所有的参数模式为
ParameterMode.IN
- 通过
TransactionalCacheManager
从二级缓存中获取结果,会装饰成TransactionalCach
对象 - 如果缓存未命中,则调用委托对象的
query
方法 - 将缓存结果保存至
TransactionalCache
对象中,并未真正的保存至Cache
二级缓存中,需要待事务提交才会保存过去,其中缓存未命中的也会设置缓存结果为null - 直接返回结果
- 如果是存储过程相关操作,保证所有的参数模式为
- 没有使用二级缓存,则调用委托对象的方法
update方法
@Override
public int update(MappedStatement ms, Object parameterObject) throws SQLException {// 如果需要清空缓存,则进行清空flushCacheIfRequired(ms);// 执行 delegate 对应的方法return delegate.update(ms, parameterObject);
}private void flushCacheIfRequired(MappedStatement ms) {Cache cache = ms.getCache();if (cache != null && ms.isFlushCacheRequired()) {tcm.clear(cache);}
}
数据库的更新操作,如果配置了需要清空缓存,则清空二级缓存
这里就和一级缓存不同,一级缓存是所有的更新操作都会清空一级缓存
commit方法
@Override
public void commit(boolean required) throws SQLException {// 执行 delegate 对应的方法delegate.commit(required);// 提交 TransactionalCacheManagertcm.commit();
}
在事务提交后,通过TransactionalCacheManager
二级缓存管理器,将本次事务生成的缓存数据从TransactionalCach
中设置到正真的Cache
二级缓存中
rollback方法
@Override
public void rollback(boolean required) throws SQLException {try {// 执行 delegate 对应的方法delegate.rollback(required);} finally {if (required) {// 回滚 TransactionalCacheManagertcm.rollback();}}
}
在事务回滚后,如果需要的话,通过TransactionalCacheManager
二级缓存管理器,将本次事务生成的缓存数据从TransactionalCach
中移除
close方法
@Override
public void close(boolean forceRollback) {try {// issues #499, #524 and #573if (forceRollback) {tcm.rollback();} else {tcm.commit();}} finally {delegate.close(forceRollback);}
}
在事务关闭前,如果是强制回滚操作,则TransactionalCacheManager
二级缓存管理器,将本次事务生成的缓存数据从TransactionalCach
中移除,否则还是将缓存数据设置到正真的Cache
二级缓存中
TransactionalCacheManager
org.apache.ibatis.cache.TransactionalCacheManager
:二级缓存管理器,因为二级缓存是支持跨 SqlSession 共享的,所以需要通过它来实现,当事务提交时,才将当前事务中查询时产生的缓存,同步到二级缓存中,代码如下:
public class TransactionalCacheManager {/*** Cache 和 TransactionalCache 的映射*/private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();public void clear(Cache cache) {getTransactionalCache(cache).clear();}public Object getObject(Cache cache, CacheKey key) {return getTransactionalCache(cache).getObject(key);}public void putObject(Cache cache, CacheKey key, Object value) {// 首先,获得 Cache 对应的 TransactionalCache 对象// 然后,添加 KV 到 TransactionalCache 对象中getTransactionalCache(cache).putObject(key, value);}public void commit() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.commit();}}public void rollback() {for (TransactionalCache txCache : transactionalCaches.values()) {txCache.rollback();}}private TransactionalCache getTransactionalCache(Cache cache) {return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new);}}
getTransactionalCache(Cache cache)
方法,根据Cache
二级缓存对象获取对应的TransactionalCache
对象,如果没有则创建一个保存起来getObject(Cache cache, CacheKey key)
方法,会先调用getTransactionalCache(Cache cache)
方法获取对应的TransactionalCache
对象,然后根据CacheKey
从该对象中获取缓存结果putObject(Cache cache, CacheKey key, Object value)
方法,同样也先调用getTransactionalCache(Cache cache)
方法获取对应的TransactionalCache
对象,根据该对象将结果进行缓存commit()
方法,遍历transactionalCaches
,依次调用TransactionalCache
的提交方法rollback()
方法,遍历transactionalCaches
,依次调用TransactionalCache
的回滚方法
TransactionalCache
org.apache.ibatis.cache.decorators.TransactionalCache
:用来装饰二级缓存的对象,作为二级缓存一个事务的缓冲区
在一个SqlSession会话中,该类包含所有需要添加至二级缓存的的缓存数据,当提交事务后会全部刷出到二级缓存中,或者事务回滚后移除这些缓存数据,代码如下:
public class TransactionalCache implements Cache {private static final Log log = LogFactory.getLog(TransactionalCache.class);/*** 委托的 Cache 对象。** 实际上,就是二级缓存 Cache 对象。*/private final Cache delegate;/*** 提交时,清空 {@link #delegate}** 初始时,该值为 false* 清理后{@link #clear()} 时,该值为 true ,表示持续处于清空状态** 因为可能事务还未提交,所以不能直接清空所有的缓存,而是设置一个标记,获取缓存的时候返回 null 即可* 先清空下面这个待提交变量,待事务提交的时候才真正的清空缓存**/private boolean clearOnCommit;/*** 待提交的 Key-Value 映射*/private final Map<Object, Object> entriesToAddOnCommit;/*** 查找不到的 KEY 集合*/private final Set<Object> entriesMissedInCache;public TransactionalCache(Cache delegate) {this.delegate = delegate;this.clearOnCommit = false;this.entriesToAddOnCommit = new HashMap<>();this.entriesMissedInCache = new HashSet<>();}@Overridepublic Object getObject(Object key) {// issue #116// <1> 从 delegate 中获取 key 对应的 valueObject object = delegate.getObject(key);if (object == null) {// <2> 如果不存在,则添加到 entriesMissedInCache 中entriesMissedInCache.add(key);}// issue #146if (clearOnCommit) {// <3> 如果 clearOnCommit 为 true ,表示处于持续清空状态,则返回 nullreturn null;} else {return object;}}@Overridepublic void putObject(Object key, Object object) {// 暂存 KV 到 entriesToAddOnCommit 中entriesToAddOnCommit.put(key, object);}@Overridepublic void clear() {// <1> 标记 clearOnCommit 为 trueclearOnCommit = true;// <2> 清空 entriesToAddOnCommitentriesToAddOnCommit.clear();}public void commit() {// <1> 如果 clearOnCommit 为 true ,则清空 delegate 缓存if (clearOnCommit) {delegate.clear();}// 将 entriesToAddOnCommit、entriesMissedInCache 刷入 delegate 中flushPendingEntries();// 重置reset();}public void rollback() {// <1> 从 delegate 移除出 entriesMissedInCacheunlockMissedEntries();// <2> 重置reset();}private void reset() {clearOnCommit = false;entriesToAddOnCommit.clear();entriesMissedInCache.clear();}private void flushPendingEntries() {for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {delegate.putObject(entry.getKey(), entry.getValue());}for (Object entry : entriesMissedInCache) {if (!entriesToAddOnCommit.containsKey(entry)) {delegate.putObject(entry, null);}}}private void unlockMissedEntries() {for (Object entry : entriesMissedInCache) {try {delegate.removeObject(entry);} catch (Exception e) {log.warn("Unexpected exception while notifiying a rollback to the cache adapter."+ "Consider upgrading your cache adapter to the latest version. Cause: " + e);}}}
}
根据上面的注释查看每个属性的作用,我们依次来看下面的方法,看看在不同事务之前是如何处理二级缓存的
putObject(Object key, Object object)
方法,添加缓存数据时,先把缓存数据保存在entriesToAddOnCommit
中,这个对象属于当前事务,事务还未提交,其他事务是不能访问到的clear()
方法,设置clearOnCommit
标记为true,告诉当前事务正处于持续清空状态,先把entriesToAddOnCommit
清空,也就是当前事务中还未提交至二级缓存的缓存数据,事务还未提交,不能直接清空二级缓存中的数据,否则影响到其他事务了commit()
方法,事务提交后,如果clearOnCommit
为true,表示正处于持续清空状态,需要先把二级缓存中的数据全部清空,然后再把当前事务生成的缓存设置到二级缓存中,然后重置当前对象
这里为什么处于清空状态把二级缓存的数据清空后,还要将当前事务生成的缓存数据再设置到二级缓存中呢?因为当前事务调用clear()
方法后可能有新生成了新的缓存数据,而不能把这些忽略掉getObject(Object key)
方法
- 先从
delegate
二级缓存对象中获取结果 - 如果缓存未命中则将该key添加到
entriesMissedInCache
属性中,因为二级缓存也会将缓存未命中的key起来,数据为null - 如果
clearOnCommit
为true,即使你缓存命中了也返回null,因为触发clear()
方法的话,本来需要清空二级缓存的,但是事务还未提交,所以先标记一个缓存持续清理的这么一个状态,这样相当于在当前事务中既清空了二级缓存数据,也不影响其他事务的二级缓存数据 - 返回获取到的结果,可能为null
Executor在哪被创建
前面对Executor执行器接口以及实现类都有分析过,那么它是在哪创建的呢?
整个的初始化入口在SqlSessionFactoryBuilder
的build
方法中,创建的是一个DefaultSqlSessionFactory
对象,该对象用来创建SqlSession
会话的,我们来瞧一瞧:
public class DefaultSqlSessionFactory implements SqlSessionFactory {private final Configuration configuration;@Overridepublic SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,boolean autoCommit) {Transaction tx = null;try {// 获得 Environment 对象final Environment environment = configuration.getEnvironment();// 创建 Transaction 对象final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 创建 Executor 对象final Executor executor = configuration.newExecutor(tx, execType);// 创建 DefaultSqlSession 对象return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {// 如果发生异常,则关闭 Transaction 对象closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);} finally {ErrorContext.instance().reset();}}
}
我们所有的数据库操作都是在MyBatis的一个SqlSession
会话中执行的,在它被创建的时候,会先通过Configuration
全局配置对象的newExecutor
方法创建一个Executor
执行器
newExecutor(Transaction transaction, ExecutorType executorType)
方法,根据执行器类型创建执行Executor
执行器,代码如下:
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {// <1> 获得执行器类型executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;// <2> 创建对应实现的 Executor 对象Executor executor;if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) {executor = new ReuseExecutor(this, transaction);} else {executor = new SimpleExecutor(this, transaction);}// <3> 如果开启缓存,创建 CachingExecutor 对象,进行包装if (cacheEnabled) {executor = new CachingExecutor(executor);}// <4> 应用插件executor = (Executor) interceptorChain.pluginAll(executor);return executor;
}
- 获得执行器类型,默认为
SIMPLE
- 创建对应的
Executor
对象,默认就是SimpleExecutor
执行器了 - 如果全局配置了开启二级缓存,则将
Executor
对象,封装成CachingExecutor
对象 - 插件链应用该对象,在后续会讲到
总结
本文分析了MyBatis在执行SQL的过程中,都是在SimpleExecutor
(默认类型)执行器中进行的,由它调用其他“组件”来完成数据库操作
其中需要通过PrepareStatementHandler
(默认)来创建对应的PrepareStatemen
,进行参数的设置等相关处理,执行数据库操作
获取到结果后还需要通过DefaultResultSetHandler
进行参数映射,转换成对应的Java对象,这两者在后续会进行分析
一级缓存
仅限于单个 SqlSession 会话,多个 SqlSession 可能导致数据的不一致性,例如某一个 SqlSession 更新了数据而其他 SqlSession 无法获取到更新后的数据,出现数据不一致性,这种情况是不允许出现了二级缓存
MyBatis配置二级缓存
是通过在XML映射文件添加<cache / >
标签创建的(注解也可以),所以不同的XML映射文件所对应的二级缓存
对象可能不是同一个二级缓存
虽然解决的一级缓存
中存在的多个 SqlSession 会话可能出现脏读的问题,但还是针对同一个二级缓存对象不会出现这种情况,如果其他的XML映射文件修改了相应的数据,当前二级缓存
获取到的缓存数据就不是最新的数据,也出现了脏读的问题
例如,在一个XML映射文件中配置了二级缓存,获取到某个用户的信息并存放在对应的二级缓存对象中,其他的XML映射文件修改了这个用户的信息,那么之前那个缓存数据就不是最新的
当然你可以所有的XML映射文件都指向同一个Cache对象(通过<cache-ref / >
标签),这样就太局限了,所以MyBatis的缓存存在一定的缺陷,且缓存的数据仅仅是保存在了本地内存中,对于当前高并发的环境下是无法满足要求的,所以我们通常不使用MyBatis的缓存
作者:月圆吖
原文链接:https://www.cnblogs.com/lifullmoon/p/14015111.html
mybatis一个方法执行多条sql_精尽MyBatis源码分析——SQL执行过程之Executor!相关推荐
- 数据库中间件 Sharding-JDBC 源码分析 —— SQL 执行
摘要: 原创出处 http://www.iocoder.cn/Sharding-JDBC/sql-execute/ 「芋道源码」欢迎转载,保留摘要,谢谢! 本文主要基于 Sharding-JDBC 1 ...
- MyBatis 源码分析 - SQL 的执行过程
本文速览 本篇文章较为详细的介绍了 MyBatis 执行 SQL 的过程.该过程本身比较复杂,牵涉到的技术点比较多.包括但不限于 Mapper 接口代理类的生成.接口方法的解析.SQL 语句的解析.运 ...
- 【Flink】 Flink 源码之 SQL 执行流程
1.概述 转载:Flink 源码之 SQL 执行流程 2.前言 本篇为大家带来Flink执行SQL流程的分析.它的执行步骤概括起来包含: 解析.使用Calcite的解析器,解析SQL为语法树(SqlN ...
- 精尽 Redisson 源码分析 —— 限流器 RateLimiter
1. 概述 限流,无论在系统层面,还是在业务层面,使用都非常广泛.例如说: [业务]为了避免恶意的灌水机或者用户,限制每分钟至允许回复 10 个帖子. [系统]为了避免服务系统被大规模调用,超过极限, ...
- 精尽 Dubbo 源码分析 —— API 配置
1. 概述 Dubbo 的配置目前提供了四种配置方式:1. API 配置 2. 属性配置 3. XML 配置 4. 注解配置 2. 配置一览 我们来看看 dubbo-config-api 的项目结构, ...
- Mybatis源码解析-sql执行
一.传统的jdbc操作步骤 获取驱动 获取jdbc连接 创建参数化预编译的sql 绑定参数 发送sql到数据库执行 将将获取到的结果集返回应用 关闭连接 传统的jdbc代码: package com. ...
- modelandview使用过程_深入源码分析SpringMVC执行过程
本文主要讲解 SpringMVC 执行过程,并针对相关源码进行解析. 首先,让我们从 Spring MVC 的四大组件:前端控制器(DispatcherServlet).处理器映射器(HandlerM ...
- 精尽 Redisson 源码分析 —— 可重入分布式锁 ReentrantLock
1. 概述 在 Redisson 中,提供了 8 种分布锁的实现,具体我们可以在 <Redisson 文档 -- 分布式锁和同步器> 中看到.绝大数情况下,我们使用可重入锁(Reentra ...
- server如何调用 thrift_一文带你了解 Thrift,一个可伸缩的跨语言 RPC 框架(pinpoint 源码分析系列)...
Thrift 是什么研究分布式调用链工具pinpoint的时候,在源码里看到了Thrift相关的代码,所以来了兴趣,想研究研究这个框架.Thrift 目前是 Apache 的一个项目,但是它是由fac ...
最新文章
- 十天精通CSS3(11)
- 浮点数在内存中的存储方式
- 获取IOS应用的子目录
- oracle数据库迁移 增大空间,Oracle数据库迁移、创建表空间、创建数据表实例讲解-Oracle...
- JS数组去重方法小结
- php中的round是什么,phpround函数怎么用
- 工业以太网交换机的接口知识详解
- [剑指offer][JAVA][面试题第13题][机器人的运动][DFS][BFS]
- 好云推荐官丨飞天加速之星怎样选择云服务器ECS?
- Java 并发编程之创建线程,启动和常用方法
- yum 安装mysql 5.0_CentOS 通过 yum 安装 Mysql 5.0
- movebase导航
- RocketMQ源码学习(六)-Name Server
- killer网卡服务器未运行,外星人killer control center服务未运行怎么解决?
- Ring3触发BSOD代码实现及内核逆向分析
- 云南网络文化经营许可证办理,云南办理网络文化经营许可证多少钱?文网文是什么?怎么办理文网文?办理文网文需要什么材料?
- 服务器停了3个月网站还能恢复吗,网站被降权怎么恢复正常(网站不更新会被降权)...
- 无internet,安全
- iamp是什么意思计算机网络,pop3和imap什么意思
- 微信为什么要做输入法?
热门文章
- Spring学习总结(21)——Spring集成阿里巴巴数据库连接池DruidDataSource配置及其常见问题汇总
- LInux下centos6.7 设置字符集,解决乱码问题
- 如何套用模板绘制生产管理流程图
- WP7有约(六):AppBarUtils使用指南
- maven项目引入新依赖问题
- IntelliJ IDEA 注释模版 输入/**后 不显示配置好的模板
- Android下基于线程池的网络访问基础框架
- AC日记——ISBN号码 openjudge 1.7 29
- shell进行mysql统计
- java selenium (十三) 智能等待页面加载完成