回顾
前面我们看了MyBatis的StatementHandler,而StatementHandler是用于跟数据库进行交互的,并且完成调用ResultSetHandler完成结果集的映射,并且StatemenHandler采用了一个装饰者模式,实际上得到的所有StatementHandler都是RoutingStatementHandler,而RoutingStatementHandler装饰了具体的StatementHandler,不过有点滑稽的是,RoutingStatementHandler并没有做任何的增强;但其实在于数据库交互之前还需要做其他操作,比如访问缓存、更新缓存\开启事务等,而负责组装这些功能的则是Executor接口
Executor
从中也可以看到了Executor分成了总共有两大实现类
- CachingExecutor:装饰器模式,提供了二级缓存的功能
- BaseExecutor:基本的Executor
- SimpleExecutor:执行一般SQL的Executor
- BatchExecutor:批量执行SQL的Executor
- ReuseExecutor:顾名思义,可以重用的Executor
下面来看一下Executor提供了哪些方法
- update:增删改操作
- query:查询操作
- close:关闭资源
- flushStatements:批量执行SQL语句
- createCacheKey:创建缓存MappedStatement的SQL需要用到的key
- isCached:判断指定MappedStatement的SQL是不是已经缓存过了
- clearLocalCache:用来清空一级缓存
- deferLoad:延迟加载缓存
- getTransaction:获取事务对象
模板方法
-
模板方法就是将某个方法的实现步骤给延迟到子类去实现,然后不同的子类就对于该方法执行的整体流程是一样的,但里面的一些步骤、算法会延迟到子类去实现,因此不同的子类,会有不同的逻辑
-
模板方法符合开放-封闭原则,当我们去对某个功能模块的某个步骤需要使用新算法、或者修改算法的时候,可以通过添加子类,然后对该步骤去具体实现即可,不需要去改动源代码,提高了系统的灵活性和可扩展性
BaseExecutor
BaseExecutor是一个抽象类,实现了Executor的大部分方法,而BaseExecutor就是一个模板
比如update、query、fushStatement方法,其实就是一个模板方法,说白了,BaseExecutor提供了与数据库交互的方法模板
比如update方法
最后调用的doUpdate是一个抽象方法,延迟到子类去实现,其他的就不一一展示了
看一下BaseExecutor的成员属性
- Transaction:事务对象有,用于事物的提交、回顾和关闭
- Executor:封装的Executor?
- deferredLoads:延迟加载队列
- localCache:一级缓存
- localOutPutParameterCache:一级缓存,用于缓存输出类型的参数
- configuration:总配置
从构造方法中可以看到,当BaseExecutor被创建,对应的一级缓存也会被创建,就是一个PerpetualCache而已
一级缓存
可以看到,一级缓存是一个PerpetualCache,也就是永久缓存,底层是一个HashMap,前面已经看过了,不过这里并没有像二级缓存一样采用装饰者模式去不断增强;所以,可以看出一级缓存其实就是一个简单的以HashMap作为底层的缓存结构而已
一级缓存简介
- 一级缓存是用来减少数据库压力的,因为数据库的连接访问是珍贵的资源,很容易成为整个系统的瓶颈;所以,为了节省这些资源,采用缓存的技术来减少数据库的负担、减少重复查询数据库的操作。
- 一级缓存是一个会话级别的缓存,也就是说,一个SqlSesson就拥有一个一级缓存,没创建一个SqlSession去访问数据库,就会开启一个一级缓存,并且一级缓存是组装在BaseExecutor中的,而一个SqlSession代表一次会话,所以一级缓存的生命周期其实就是会话的生命周期,当会话关闭,对应的一级缓存也会消亡,一次会话其实就代表连接一次数据库;所以一级缓存仅仅只是优化了一次会话中的重复查询,当新的会话继续重复查询,依然会与数据库建立连接,要想突破这个限制,还是需要程序员去主动添加、维护Redis缓存
一级缓存的管理
- 当执行访问操作的时候,可能会使用到缓存,也就是执行query方法的时候
- 步骤如下
- 先创建CacheKey对象
- 使用CacheKey对象查找一级缓存
- 如果命中,直接从缓存中取出返回
- 如果没命中,查询数据库,也就是使用StatementHandler去查询数据库,然后查询出ResultSet,交由ResultHandler去完成结果集映射工作,将结果对象保存到一级缓存中
所以,下面来看query方法
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
//获取绑定的SQL,并且这里注入了实参
//后续StatementHandler就能使用BoundSql来完成实参映射
BoundSql boundSql = ms.getBoundSql(parameter);
//创建CacheKey
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
//调用重载的query方法
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
CacheKey如何生成
下面来看看CacheKey是如何生成的,对应的方法是createCacheKey
public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//CacheKey对象
CacheKey cacheKey = new CacheKey();
//MappedStatement的ID属性:
//也就是命名空间+MappedStatement对应的SQL结点的ID属性
//通常这样子组合成就成了要执行的接口方法的全限定名了
cacheKey.update(ms.getId());
//分页的起始偏移
cacheKey.update(rowBounds.getOffset());
//分页的终止位置
cacheKey.update(rowBounds.getLimit());
//绑定的SQL,该SQL是解析完了#{}占位符的SQL,也就是#{}占位符被替换成?的SQL
cacheKey.update(boundSql.getSql());
//获取#{}占位符映射的实参
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
//获取基本类型转换注册中心
//基本上这玩意是用来判断是不是基础类型的!
//只要注册进去的类型都会视为基础类型的
TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
// mimic DefaultParameterHandler logic
// 遍历需要映射的参数对象
for (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) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
//使用MetaObject去获取
//MetaObject可以获取对象中的值,根据get方法去获取
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
//添加映射的实参
cacheKey.update(value);
}
}
//同时还会去添加总配置环境的环境
if (configuration.getEnvironment() != null) {
//也就是总配置文件正在使用的Environment标签的id属性,
//也就是数据源的ID
cacheKey.update(configuration.getEnvironment().getId());
}
return cacheKey;
}
从中可以看到CacheKey的与哪些因素相关
- 执行SQL结点的ID和命名空间,起始也就是接口方法的全限定名字
- 分页的偏移量
- 分页的中止位置
- 绑定的SQL
- 使用到的映射实参
- 数据源的ID,也就是Environment标签的id属性
但从上面可以看出,CacheKey使用的是update方法去进行关联这些因素,下面来看看update方法是怎么关联这些因素的
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
前面我们已经看过CacheKey了,起始就是将影响因素都放在一个ArrayList中,并且去维护hashCode属性
重载的query方法
当生成了CacheKey之后,就会去调用重载的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 (closed) {
throw new ExecutorException("Executor was closed.");
}
//首次查询,并且执行的SQL结点开启了flushCache属性
//那么在首次查询的时候就会去清空缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
//进行查询
List<E> list;
try {
//query查询入栈,表示查询层数
queryStack++;
//从一级缓存中获取
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
//如果不为Null
if (list != null) {
//证明缓存中有值
//处理一级缓存的入参
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
//如果缓存中没有,调用queryFromDatabase方法去查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
//查询完,出栈
queryStack--;
}
//如果查询层数为0,进行延迟加载
//嵌套查询的时候要用到,可前面没有分析过嵌套查询,直接跳过了。。。
//嵌套子查询
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
//如果一级缓存被修改为 Statement级别,也就是语句级别的
//会清空缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
//返回结果
return list;
}
- query会通过CacheKey对象去判断一级缓存中是否有对应缓存项
- 如果有,返回缓存项
- 如果没有,查询数据库
- 并且在查询前,会使用一个整形来代表入栈,也就是queryStack,该queryStack会进行自增
- 当查询完将要返回对象时,queryStack会自减
- 但到栈顶的时候queryStack为0,此时就要进行延迟加载的对象进行加载,其实就是嵌套子查询的结果集!
- 并且最后会判断,当前的MyBatis总配置是否将LocalCacheScope属性改为statement,如果改为statement,代表了一级缓存将会是SQL级别的,一旦执行完SQL就会被清空,所以,一级缓存的级别是有两种的,会话级别和SQL级别,默认为会话级别,SQL级别需要开启,但一级缓存的生命周期肯定是与会话的声明周期相等,但如果是SQL级别,只是一级缓存会被清空而已
update方法的细节
insert、update、delete结点都是调用update方法
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
//先清空缓存
clearLocalCache();
//然后调用doUpdate
return doUpdate(ms, parameter);
}
从中可以看到,只要调用了update方法,一级缓存就会被全部清空,然后调用doUpdate方法,而doUpdate方法则是延迟到对应子类去实现的
下面再看看什么操作会清空一级缓存
可以看到,commit方法会清空、rollback方法也会清空
一级缓存清空的情况
-
执行query方法,第一次查询,并且select语句开启了flushCache属性会清空
-
执行query方法,并且MyBatis总配置文件设置了localCacheScope属性为Statement等级,也就是一级缓存的等级为SQL级别,此时每完成一次query方法,最后都会清空一级缓存
-
执行update方法,也就是增删改操作都会清空一级缓存
-
事务提交会清空一级缓存,也就是执行commit方法
-
事务回滚也会清空一级缓存,也就是执行rollback方法
-
并且注意,清空一级缓存是一级输出参数缓存(存储过程使用)和一级缓存都会被清空
事务相关操作的细节
flushStatement方法
flushStatement其实就是用来批量执行SQL的,将存在缓冲区的SQL一次性全部执行
事务提交
可以看到,事务提交之前,需要做两件事情
- 清空两个一级缓存
- 刷新缓存的所有Statement
- 最后事务进行提交
回滚操作
回滚操作跟提交操作类似,都是要先清空一级缓存、然后刷新缓存中的Statement,最后才会执行事务回滚