本文阐述了Mybatis的查询执行流程,包含Session的获取、数据源信息加载以及查询过程中使用的一级、二级缓存。有疏漏之处,还望指出。
1.获取mybatis定义的xml文件。加载数据源配置信息。得到InputStream。
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
2.获取DefaultSqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
这一步加载mybaits配置,解析xml文件得到Document对象,保存在SqlSessionFactory的Configuration
build时使用Configuration构造DefaultSqlSessionFactory,加载xml mapper,生成MappedStatement放入Configuration中的HashMap中缓存。
DefaultSqlSessionFactory打开session
SqlSession session = sqlSessionFactory.openSession();
openSession执行流程:
Environment environment = configuration.getEnvironment();
DefaultSqlSessionFactory中的openSession,获取Configuration中的Environment。Environment含有数据连接的所有信息(URL,PWD,USER AND ETC)。
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
通过environment获得事务工厂TransactionFactory,产生JdbcTransaction事务。JdbcTransaction持有
Connection,可以控制Connection的commit、rollback、close等方法。
Executor executor = configuration.newExecutor(tx, execType);
通过configuration的newExecutor方法使用JdbcTransaction新建Executor。
Executor有四种:
- BatchExecutor
- ReuseExecutor
- SimpleExecutor
- CachingExecutor
默认为CachingExecutor
Executor拥有对数据操作的全部方法。如query、update、commit(通过事务实现)。这里CachingExecutor为Executor的装饰者。
创建Executor的最后一步是植入插件pluginAll---https://www.cnblogs.com/xrq730/p/6984982.html
executor = (Executor) interceptorChain.pluginAll(executor);
(PS:生成插件共有四处:ParameterHandler/ResultHandler/StatementHandler/Executor)
IntercptorChain中加载有Configuration的来自于XML定义的插件。IntercptorChain对所有的插件进行plugin().Plugin是一个InvocationHandler,代理执行器。Plugin会调用wrap对原始对象(3中四种对象)进行包装,获取插件的类、接口、方法信息后,通过Proxy生成代理类,返回代理对象。当对象调用插件拦截的方法时,InvocationHandler会调用插件的intercept方法。
5.构建DefaultSqlSession。使用Configuration、executor创建DefaultSqlSession。
至此,SqlSession创建完成。
查询SelectOne:
1.DefaultSqlSession执行SelectOne
获取Configuration中的MappedStatement.调用executor的query方法,传入MappedStatement、查询 参数、查询行数。
MappedStatement ms = configuration.getMappedStatement(statement);
executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
使用MappedStatement创建查询Sql,并生成缓存的Key。这里的缓存为(第)二级缓存,执行清缓存操作。
缓存原理:
判断是否缓存命中,命中直接取出返回。否则代理的方式调用BaseExecutor的query方法委托查询。
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);// 获取二级缓存
if (list == null) {
//装饰者模式调用 BaseExecutor
list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
return list;
}
}
return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);//不使用缓存
其中,二级缓存为TransactionalCacheManager持有的一个Map。
Map<Cache, TransactionalCache> transactionalCaches = new HashMap<>();
二级缓存是跨 Session 共享缓存。随着session的commit,缓存清空失效。
一级缓存是Session级别共享。
BaseExecutor的query方法首先进行一级缓存判断。缓存使用PerpetualCache实现。PerpetualCache为最基础的缓存类,使用Map实现。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// STATEMENT为语句级
// SESSION为会话级
// issue #482
clearLocalCache();
}
二级缓存补充:
对于查询多commit少且用户对查询结果实时性要求不高,此时采用mybatis二级缓存技术降低数据库访问量,提高访问速度。
但不能滥用二级缓存,二级缓存也有很多弊端,从MyBatis默认二级缓存是关闭的就可以看出来。
二级缓存是建立在同一个namespace下的,如果对表的操作查询可能有多个namespace,那么得到的数据就是错误的。
举个简单的例子:
订单和订单详情,orderMapper、orderDetailMapper。在查询订单详情时我们需要把订单信息也查询出来,那么这个订单详情的信息被二级缓存在orderDetailMapper的namespace中,这个时候有人要修改订单的基本信息,那就是在orderMapper的namespace下修改,他是不会影响到orderDetailMapper的缓存的,那么你再次查找订单详情时,拿到的是缓存的数据,这个数据其实已经是过时的。
根据以上,想要使用二级缓存时需要想好两个问题:
1)对该表的操作与查询都在同一个namespace下,其他的namespace如果有操作,就会发生数据的脏读。
2)对关联表的查询,关联的所有表的操作都必须在同一个namespace。