前面知道了SqlSession 初始化过程,那么下一步就看看Mybatis具体的增删改查逻辑。
本文以以下几个问题开展:
- Mybatis Mapper 代理对象获取流程
- 动态sql查询时,if,foreach等节点是怎么处理的?
- Mybatis动态Sql对如何读取参数的?
- @Param注解什么作用,什么情况没有@Param注解会读取不到参数?
- 对于结果集,Mybatis是如何处理的?
- Mybatis 一级二级缓存如何使用?
Mybatis代理对象
当使用 Mybatis是,我们只需要定义接口,而后定义对应 xml文件,就可以完成增删改查,接口对象怎么创建呢?
- 通过SqlSession 的
getMapper
:
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
-
Configuration
中 的getMapper
:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return mapperRegistry.getMapper(type, sqlSession);
}
MapperRegistry
中保存了来自 xml配置下所有Mappers,并放到一个 Map<Class<?>, MapperProxyFactory<?>>
保存所有接口文件对应的 Mapper
构造器。
- 最后在
MapperRegistry
的 通过 在初始化中构造好的MapperProxyFactory
的newInstance
,生成一个 由MapperProxy
包装的动态对象。
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
而后通过JDK 反射获取对应接口:
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
以上返回了一个动态代理类,该类实现了 mapperInterface
接口,并且以 MapperProxy
作为默认包装代理。
所以当使用 执行代理类方法,首先会进入 MapperProxy
的invoke
方法。
Select 查询分析
Mybatis 整体流程是比较好理解的,好理解的前提起始是我们都忽略了 其强大的XML解析配置,以及对动态SQL的强大支持。下面看看查询:
- 执行对应查询方法,例如
mapper.listAllActivedUsers(actived, list);
- 进入
MapperProxy
的invoke
方法:期间会创建一个MapperMethod
类,用于构建 代表增删改查的SqlComand
;以及方法签名包装类MethodSignature
,里面放着其返回值,是否返回list,是否返回的Cursor
等。 -
MapperMethod
中,主要是判断SqlComand
类型,增删改还是查,最后返回查询初的结果。 - 当Mybatis 判断为查询多个时候,则会进入
MapperMethod
的excuteForMany
方法,这里面重要的代码是Object param = method.convertArgsToSqlCommandParam(args);
,param是解析 参数后的 Map。
它会将传入参数以<key,value>
存储,以@Param
的值作为key,并且还会固定以参数顺序存储一份param1, param2, param3
等固定名字参数,所以param
中容量是实际传入的一倍 - 判断是否有传入
RowBounds
分页参数,有则加载进入。
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.selectList(command.getName(), param);
}
...
- 在
DefaultSqlSession
对其进行 查询,其中会如果参数是collection
或者数组,则会进行封装一层。 - 动态sql 拼装逻辑看下一节
- 查询时,首先会执行
CachingExecutor
的query
,这一层主要对 一级缓存进行相应逻辑判断,一级缓存默认开启:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 判断 MappedStatement 是否有缓存
Cache cache = ms.getCache();
if (cache != null) {
// 有cache 判断是否要删除缓存
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
@SuppressWarnings("unchecked")
// 从 TransactionalCacheManager 加载缓存
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 查询后加入缓存
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);
}
- 而后拿着句柄就往下执行
BaseExecutor
的query
方法, 在 里面有个 对ErrorContext
进行的设定,关于ErrorContext
可以看下下节。 - 如果第一次 查询,则会清除
localCache
和localOutputParameterCache
缓存,而下面对该次查询进行了重入性判断,当再次查询时则会直接从localCache
获取。list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
- 下一步即从数据库中查询,默认使用
SimpleExecutor
进行 操作,应此Statement
也是一次性使用
@Override
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, 也包括其中对过滤器链的操作
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 执行 prepare,即预检查
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
prepareStatement:
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
stmt = handler.prepare(connection, transaction.getTimeout());
handler.parameterize(stmt);
return stmt;
}
- 最后就是在
PreparedStatementHandler
执行 sql,执行完后 使用DefaultResultSetHandler.handleResultSets
处理结果。 - 处理查询结果主要将 查出结果
ResultSet
封装成结果返回,中途还会 处理 自定义ResultSet
以及Lazy Loading
相关。下面基于DefaultResultSetHandler
的handleResultSets
:
@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
// 设置当前动作
ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
final List<Object> multipleResults = new ArrayList<>();
// 设置检索
int resultSetCount = 0;
// 从 Statment 中获取 ResultSet 并封装
ResultSetWrapper rsw = getFirstResultSet(stmt);
// 获取ResultMap
List<ResultMap> resultMaps = mappedStatement.getResultMaps();
// 计算resultMaps 数量
int resultMapCount = resultMaps.size();
// 简单校验result map 数量
validateResultMapsCount(rsw, resultMapCount);
while (rsw != null && resultMapCount > resultSetCount) {
// 获取resultMap
ResultMap resultMap = resultMaps.get(resultSetCount);
// 处理结果成 resultMap形式,并放入multipleResult
handleResultSet(rsw, resultMap, multipleResults, null);
// 下一个resultSetWrapper
rsw = getNextResultSet(stmt);
// 将当前nestedResultObjects 内容
cleanUpAfterHandlingResultSet();
// 如果有则下一个
resultSetCount++;
}
String[] resultSets = mappedStatement.getResultSets();
if (resultSets != null) {
// 嵌套查询
while (rsw != null && resultSetCount < resultSets.length) {
ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
if (parentMapping != null) {
String nestedResultMapId = parentMapping.getNestedResultMapId();
ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
handleResultSet(rsw, resultMap, null, parentMapping);
}
rsw = getNextResultSet(stmt);
cleanUpAfterHandlingResultSet();
resultSetCount++;
}
}
// 封装成list返回
return collapseSingleResultList(multipleResults);
}
动态SQL解析
下一个关键点就在 Mybatis的ongl表达式解析了,或许会有这样一个问题,为什么传入的参数mybatis可以理解,并且可以以动态sql形式解析,支持 if 和 foreach 等逻辑?
在 MappedStatement
的 getBoundSql
可以获取一个BoundSql
,BoundSql
对象是一个从 SqlSource
中分析之后获取的 动态对象。最终 能从 xml 对象中将 动态sql解析出含有 ?
和 参数的字段。
- 从 sqlSource中获取对象:
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
- 而后,在
DynamicSqlSource
中的getBoundSql
进行进一步解析,在DynamicSqlSource
中有变量SqlNode
, 动态sql主要组装逻辑就在SqlNode
子类中。
上面 SqlNode大致作用如下:
-
StaticTextSqlNode
: 里面有个String
类型text
,主要是匹配xml中静态sql -
MixedSqlNode
: 里面有List<SqlNode>
,主要是装载多种混合类型SqlNode
,即可以为多种SqlNode
的组合 -
TextSqlNode
:用于存储解析 包含${}
占位符的动态SQL节点。将动态SQL,带${}
解析完成SQL语句的解析其,即将${}
占位符替换成实际的变量值。 -
ForEachSqlNode
:用于存储解析<foreach>
节点值。 -
IfSqlNode
: 用于解析<if>
节点值,由于<if>
下面可能又是一个节点,所以里面还有一个SqlNode contents
。 -
VarDeclSqlNode
:处理动态xml标签<bind>
的SqlNode类。 -
TrimSqlNode
: 用于处理<trim>
节点 的 SqlNode。 -
ChooseSqlNode
: 用于处理<choose>
标签的工具类。
DynamicSqlSource 的 getBoundSql:
@Override
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 递归的处理动态sql节点
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
上面代码中,使用 rootSqlNode.apply(context);
解析动态sql节点,而后使用 SqlSourceBuilder
,将 #
开头的占位符,全部使用 ?
替代,而后将解析出来参数设置到上下文 DynamicContext
中。
另外,对于处理 <foreach>
的逻辑,是将所有参数都以 ?
填充,而增加相应个数参数从而可以适配循环参数。
ErrorContext 作用
当使用Mybatis时候,是否会发现这样的感觉,如果哪个环节出错,能够迅速的找出问题,或者sql少写了一个点,或者 哪个 <if>
标签判断出错导致参数错误,或者是参数或者结果解析出问题,都能马上定位到问题。
这不是你厉害,而是Mybatis告诉你哪里出错了>-<
Mybatis就是使用ErrorContext 来帮你记录你sql运行的过程。
例如 : ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
private ErrorContext stored;
private String resource;
private String activity;
private String object;
private String message;
private String sql;
private Throwable cause;
看到 static
的 ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();
是一个很典型的 ThreadLocal
用法,每一次与数据库交互的查询,都能够被当前上下文保存下来。ErrorContext
的构造方法是私有的,使用 instance
创建而成的 单例模式。
-
resource
:说明是加载哪一个资源,主要为 xml的Mapper文件 -
activity
:说明什么动作,例如查询就是executing a query
-
object
:说明是执行mapper文件中,哪个语句,例如anla.learn.mybatis.interceptor.dao.UserMapper.listAllActivedUsers
message
: 用于保存出错信息,调用点为ExceptionFactory
,而传入的message主要为 特定错误类型:-
sql
:存入boundSql
中的sql,也就是通过动态sql解析过后的sql。在BaseStatementHandler
中的prepare
调用,所以所有与数据库交互语句都会在这里初始化 sql。 -
cause
: 主要存储错误栈,配合 上面message
使用。
在每次执行完并且没有错,都会将当前 ErrorContext
清除。
一级缓存和二级缓存
其实一级缓存二级缓存命名比较别扭,因为事实上,如果开启了二级缓存,则最终缓存调用顺序为:
二级缓存->一级缓存->数据库。因为二级默认不开启,所以一般是一级缓存,如果一级缓存中没有,则到数据库。
一二级缓存这样区分,主要是Mybatis上有相关注释说明了 TransactionalCacheManager
为二级缓存。
- 一级缓存:MyBatis提供了一级缓存的方案优化这部分场景,如果是相同的SQL语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
- 二级缓存:
二级缓存存储位置是CachingExecutor
的TransactionalCacheManager
中,默认不开启。
下面简单分析下一二级缓存及其原理:
一级缓存
一级缓存在BaseExecutor
的 PerpetualCache
中,默认开启。作用域默认是 SESSION,可以选择为STATEMENT。如果为SESSION,则在整个SqlSession中都会共享该缓存,而如果为Statement,则本次查询结束,则会清除该缓存。
@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());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 看是否需要清空缓存
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();
// 如果是STATEMENT 域缓存,则用完直接清空
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
一级缓存实现原理较为简单, PerpetualCache
里面主要 定义一个 Map<Object, Object> cache
类型全局变量,实现了缓存操作。
总结:
- 一级缓存默认开启,默认为 SESSION级别
- Mybatis 一级缓存声明周期和 SqlSession一致,数据只在 SqlSession内部(配置为SESSION时)
- Mybatis一级缓存设计为一个没有容量的HashMap。
- 以及缓存范围为SqlSession,在多个SqlSession或者分布式下,会引起脏数据,建议设定为 STATEMENT。
二级缓存
二级缓存主要使用实际上在一级缓存之前就会调用,在 CachingExecutor
的 query
中,但是需要设置Setting:
<setting name="cacheEnabled" value="true"/>
实际在 3.5.2 中, 这个默认是开启。
第二个是需要在xml中增加 <cache/>
标签。并配置好namespace
从而在 读取配置时,会初始化 MappedStatement
中Cache,在 CachingExecutor
中会进行引用:
@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 如果配置了 二级缓存以及相应namespace,则会有Cache
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) {
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);
}
二级缓存开启后,同一个namespace下的所有操作语句,都影响着同一个Cache,即二级缓存被多个SqlSession共享,是一个全局的变量。