MyBatis 源码分析之 Select 语句执行(上)
创始人
2024-02-05 07:18:22
  • 三哥

内容来自【自学星球】

欢迎大家来了解我的星球,和星主(也就是我)一起学习 Java ,深入 Java 体系中的所有技术。我给自己定的时间是一年,无论结果如何,必定能给星球中的各位带来点东西。

想要了解更多,欢迎访问👉:自学星球

--------------SSM系列源码文章及视频导航--------------

创作不易,望三连支持!

SSM源码解析视频

👉点我

Spring

  1. Spring 中注入 Bean 的各种骚操作做
  2. Spring 中Bean的生命周期及后置处理器使用
  3. Spring 中容器启动分析之refresh方法执行之前
  4. Spring refresh 方法分析之一
  5. Spring refresh 方法之二 invokeBeanFactoryPostProcessors 方法解析
  6. Spring refresh 方法分析之三
  7. Spring refresh 方法之四 finishBeanFactoryInitialization 分析
  8. Spring AOP源码分析一
  9. Spring AOP源码分析二
  10. Spring 事务源码分析

SpringMVC

  1. SpringMVC 启动流程源码分析
  2. SpringMVC 请求流程源码分析

MyBatis

  1. MyBatis 源码分析之 SqlSessionFactory 创建
  2. MyBatis 源码分析之 SqlSession 创建
  3. MyBatis 源码分析之 Mapper 接口代理对象生成及方法执行
  4. MyBatis 源码分析之 Select 语句执行(上)
  5. MyBatis 源码分析之 Select 语句执行(下)
  6. MyBatis 源码分析一二级缓存

---------------------【End】--------------------

一、Select 语句执行

接上回分析:

result = sqlSession.selectOne(command.getName(), param);

org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)

public  T selectOne(String statement, Object parameter) {// Popular vote was to return null on 0 results and throw exception on too many.// 调用 selectList 获取结果List list = this.selectList(statement, parameter);if (list.size() == 1) {// 返回结果return list.get(0);} else if (list.size() > 1) {// 如果查询结果大于1则抛出异常throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());} else {return null;}
}

该方法会调用 selectList 获取结果,然后对结果进行判断,结果大于 1 则报错反之则返回结果。

下面我们来看看 selectList 方法的实现。

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object)

public  List selectList(String statement, Object parameter) {// 调用重载方法return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)

public  List selectList(String statement, Object parameter, RowBounds rowBounds) {try {// 通过MappedStatement的Id获取 MappedStatementMappedStatement ms = configuration.getMappedStatement(statement);// 调用 Executor 实现类中的 query 方法return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);} finally {ErrorContext.instance().reset();}
}

在从 configuration 中获取 MappedStatement 之后就直接交给 Executor 的 query 方法执行了。

Executor 的创建可以回顾,4.2 节,下面我们来看看 query 方法的源码。

org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获取 BoundSqlBoundSql boundSql = ms.getBoundSql(parameterObject);// 创建 CacheKeyCacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);// 调用重载方法return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

该方法不是真正的查询方法,但查询之前做了两个很重要的步骤:

  • 获取 SQL
  • 创建缓存 key

下面我们分析获取 BoundSql 。

1.1 获取 BoundSql

// 获取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);

调用了MappedStatement的getBoundSql方法,并将运行时参数传入其中,我们大概的猜一下,这里是不是拼接SQL语句呢,并将运行时参数设置到SQL语句中?

我们都知道 SQL 是配置在映射文件中的,但由于映射文件中的 SQL 可能会包含占位符 #{},以及动态 SQL 标签,比如 、 等。因此,我们并不能直接使用映射文件中配置的 SQL。MyBatis 会将映射文件中的 SQL 解析成一组 SQL 片段。我们需要对这一组片段进行解析,从每个片段对象中获取相应的内容。然后将这些内容组合起来即可得到一个完成的 SQL 语句,这个完整的 SQL 以及其他的一些信息最终会存储在 BoundSql 对象中。下面我们来看一下 BoundSql 类的成员变量信息,如下:

public class BoundSql {// 一个完整的 SQL 语句,可能会包含问号 ? 占位符private final String sql;// 参数映射列表,SQL 中的每个 #{xxx} 占位符都会被解析成相应的 ParameterMapping 对象private final List parameterMappings;// 运行时参数,即用户传入的参数,比如 Article 对象,或是其他的参数private final Object parameterObject;// 附加参数集合,用于存储一些额外的信息,比如 datebaseId 等private final Map additionalParameters;// additionalParameters 的元信息对象private final MetaObject metaParameters;
}

接下来我们接着MappedStatement 的 getBoundSql 方法,源码如下:

org.apache.ibatis.mapping.MappedStatement#getBoundSql

public BoundSql getBoundSql(Object parameterObject) {// 获取BoundSql对象,BoundSql对象是对动态sql的解析BoundSql boundSql = sqlSource.getBoundSql(parameterObject);// 获取参数映射List parameterMappings = boundSql.getParameterMappings();if (parameterMappings == null || parameterMappings.isEmpty()) {boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);}// check for nested result maps in parameter mappings (issue #30)for (ParameterMapping pm : boundSql.getParameterMappings()) {String rmId = pm.getResultMapId();if (rmId != null) {ResultMap rm = configuration.getResultMap(rmId);if (rm != null) {hasNestedResultMaps |= rm.hasNestedResultMaps();}}}return boundSql;
}

MappedStatement 的 getBoundSql 在内部调用了 SqlSource 实现类的 getBoundSql 方法,并把 method 运行时参数传进去,SqlSource 是一个接口,它有如下几个实现类:

  • DynamicSqlSource
  • RawSqlSource
  • StaticSqlSource
  • ProviderSqlSource
  • VelocitySqlSource

当 SQL 配置中包含 ${}(不是 #{})占位符,或者包含 、 等标签时,会被认为是动态 SQL,此时使用 DynamicSqlSource 存储 SQL 片段。否则,使用 RawSqlSource 存储 SQL 配置信息。我们来看看 DynamicSqlSource的 getBoundSql 。

org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql

public BoundSql getBoundSql(Object parameterObject) {// 创建 DynamicContextDynamicContext context = new DynamicContext(configuration, parameterObject);// 解析 SQL 片段,并将解析结果存储到 DynamicContext 中,// 这里会将${}替换成method对应的运行时参数,也会解析等SqlNoderootSqlNode.apply(context);SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass();/** 构建 StaticSqlSource,在此过程中将 sql 语句中的占位符 #{} 替换为问号 ?,* 并为每个占位符构建相应的 ParameterMapping*/SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());// 调用 StaticSqlSource 的 getBoundSql 获取 BoundSqlBoundSql boundSql = sqlSource.getBoundSql(parameterObject);// 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中for (Map.Entry entry : context.getBindings().entrySet()) {boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());}return boundSql;
}

总结一下该方法的执行步骤:

  1. 创建 DynamicContext
  2. 解析 SQL 片段,并将解析结果存储到 DynamicContext 中
  3. 解析 SQL 语句,并构建 StaticSqlSource
  4. 调用 StaticSqlSource 的 getBoundSql 获取 BoundSql
  5. 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql

1.1.1 DynamicContext

DynamicContext 是 SQL 语句构建的上下文,每个 SQL 片段解析完成后,都会将解析结果存入 DynamicContext 中。待所有的 SQL 片段解析完毕后,一条完整的 SQL 语句就会出现在 DynamicContext 对象中。

public class DynamicContext {public static final String PARAMETER_OBJECT_KEY = "_parameter";public static final String DATABASE_ID_KEY = "_databaseId";//bindings 则用于存储一些额外的信息,比如运行时参数private final ContextMap bindings;//sqlBuilder 变量用于存放 SQL 片段的解析结果private final StringBuilder sqlBuilder = new StringBuilder();public DynamicContext(Configuration configuration, Object parameterObject) {// 创建 ContextMap,并将运行时参数放入ContextMap中if (parameterObject != null && !(parameterObject instanceof Map)) {MetaObject metaObject = configuration.newMetaObject(parameterObject);bindings = new ContextMap(metaObject);} else {bindings = new ContextMap(null);}// 存放运行时参数 parameterObject 以及 databaseIdbindings.put(PARAMETER_OBJECT_KEY, parameterObject);bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());}public void bind(String name, Object value) {bindings.put(name, value);}//拼接Sql片段public void appendSql(String sql) {sqlBuilder.append(sql);sqlBuilder.append(" ");}//得到sql字符串public String getSql() {return sqlBuilder.toString().trim();}//继承HashMapstatic class ContextMap extends HashMap {private MetaObject parameterMetaObject;public ContextMap(MetaObject parameterMetaObject) {this.parameterMetaObject = parameterMetaObject;}@Overridepublic Object get(Object key) {String strKey = (String) key;// 检查是否包含 strKey,若包含则直接返回if (super.containsKey(strKey)) {return super.get(strKey);}if (parameterMetaObject != null) {// issue #61 do not modify the context when reading// 从运行时参数中查找结果,这里会在${name}解析时,通过name获取运行时参数值,替换掉${name}字符串return parameterMetaObject.getValue(strKey);}return null;}}// 省略部分代码}  

1.1.2 解析 SQL 片段

接着我们来看看解析SQL片段的逻辑

rootSqlNode.apply(context);

对于一个包含了 ${} 占位符,或 、 等标签的 SQL,在解析的过程中,会被分解成多个片段。每个片段都有对应的类型,每种类型的片段都有不同的解析逻辑。在源码中,片段这个概念等价于 sql 节点,即 SqlNode。

SqlNode 是一个接口,其有很多种实现,类基础图如下:

在这里插入图片描述

  • StaticTextSqlNode 用于存储静态文本

  • TextSqlNode 用于存储带有 ${} 占位符的文本

  • IfSqlNode 则用于存储 节点的内容

  • MixedSqlNode 内部维护了一个 SqlNode 集合,用于存储各种各样的 SqlNode

接下来,我将会对 MixedSqlNode 、StaticTextSqlNode、TextSqlNode、IfSqlNode、WhereSqlNode 以及 TrimSqlNode 等进行分析。

MixedSqlNode

public class MixedSqlNode implements SqlNode {private final List contents;public MixedSqlNode(List contents) {this.contents = contents;}@Overridepublic boolean apply(DynamicContext context) {// 遍历 SqlNode 集合for (SqlNode sqlNode : contents) {// 调用 salNode 对象本身的 apply 方法解析 sqlsqlNode.apply(context);}return true;}
}

MixedSqlNode 可以看做是 SqlNode 实现类对象的容器,凡是实现了 SqlNode 接口的类都可以存储到 MixedSqlNode 中,包括它自己。MixedSqlNode 解析方法 apply 逻辑比较简单,即遍历 SqlNode 集合,并调用其他 SqlNode实现类对象的 apply 方法解析 sql。

StaticTextSqlNode

public class StaticTextSqlNode implements SqlNode {private final String text;public StaticTextSqlNode(String text) {this.text = text;}@Overridepublic boolean apply(DynamicContext context) {//直接拼接当前sql片段的文本到DynamicContext的sqlBuilder中context.appendSql(text);return true;}}

StaticTextSqlNode 用于存储静态文本,直接将其存储的 SQL 的文本值拼接到 DynamicContext 的sqlBuilder中即可。

TextSqlNode

public class TextSqlNode implements SqlNode {private final String text;private final Pattern injectionFilter;@Overridepublic boolean apply(DynamicContext context) {// 创建 ${} 占位符解析器GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));// 解析 ${} 占位符,通过ONGL 从用户传入的参数中获取结果,替换text中的${} 占位符// 并将解析结果的文本拼接到DynamicContext的sqlBuilder中context.appendSql(parser.parse(text));return true;}private GenericTokenParser createParser(TokenHandler handler) {// 创建占位符解析器return new GenericTokenParser("${", "}", handler);}private static class BindingTokenParser implements TokenHandler {private DynamicContext context;private Pattern injectionFilter;public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {this.context = context;this.injectionFilter = injectionFilter;}@Overridepublic String handleToken(String content) {Object parameter = context.getBindings().get("_parameter");if (parameter == null) {context.getBindings().put("value", null);} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {context.getBindings().put("value", parameter);}// 通过 ONGL 从用户传入的参数中获取结果Object value = OgnlCache.getValue(content, context.getBindings());String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"// 通过正则表达式检测 srtValue 有效性checkInjection(srtValue);return srtValue;}}
}   

GenericTokenParser 是一个通用的标记解析器,用于解析形如 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89pa28b1-1668829789564)(https://math.jianshu.com/math?formula=%7Bname%7D%EF%BC%8C%23%7Bid%7D%20%E7%AD%89%E6%A0%87%E8%AE%B0%E3%80%82%E6%AD%A4%E6%97%B6%E6%98%AF%E8%A7%A3%E6%9E%90)]{name}的形式,从运行时参数的Map中获取到key为name的值,直接用运行时参数替换掉 ${name}字符串,将替换后的text字符串拼接到DynamicContext的sqlBuilder中

举个例子吧,比喻我们有如下SQL

SELECT * FROM user WHERE name = '${name}' and id= ${id}

假如我们传的参数 Map中name值为 chenhao,id为1,那么该 SQL 最终会被解析成如下的结果:

SELECT * FROM user WHERE name = 'chenhao'; DROP TABLE user;#'

由于传入的参数没有经过转义,最终导致了一条 SQL 被恶意参数拼接成了两条 SQL。这就是为什么我们不应该在 SQL 语句中是用 ${} 占位符,风险太大。

IfSqlNode

public class IfSqlNode implements SqlNode {private final ExpressionEvaluator evaluator;private final String test;private final SqlNode contents;public IfSqlNode(SqlNode contents, String test) {this.test = test;this.contents = contents;this.evaluator = new ExpressionEvaluator();}@Overridepublic boolean apply(DynamicContext context) {// 通过 ONGL 评估 test 表达式的结果if (evaluator.evaluateBoolean(test, context.getBindings())) {// 若 test 表达式中的条件成立,则调用其子节点节点的 apply 方法进行解析// 如果是静态SQL节点,则会直接拼接到DynamicContext中contents.apply(context);return true;}return false;}}

IfSqlNode 对应的是 节点,首先是通过 ONGL 检测 test 表达式是否为 true,如果为 true,则调用其子节点的 apply 方法继续进行解析。如果子节点是静态SQL节点,则子节点的文本值会直接拼接到DynamicContext中

好了,其他的 SqlNode 我就不一一分析了,大家有兴趣的可以去看看

1.1.3 解析 #{} 占位符

经过前面的解析,我们已经能从 DynamicContext 获取到完整的 SQL 语句了。但这并不意味着解析过程就结束了,因为当前的 SQL 语句中还有一种占位符没有处理,即 #{}。与 ${} 占位符的处理方式不同,MyBatis 并不会直接将 #{} 占位符替换为相应的参数值,而是将其替换成?。其解析是在如下代码中实现的

SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());

org.apache.ibatis.builder.SqlSourceBuilder#parse

public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) {// 创建 #{} 占位符处理器ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);// 创建 #{} 占位符解析器GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);// 解析 #{} 占位符,并返回解析结果字符串String sql = parser.parse(originalSql);// 封装解析结果到 StaticSqlSource 中,并返回,因为所有的动态参数都已经解析了,可以封装成一个静态的SqlSourcereturn new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

该方法通过创建占位符的处理器和解析器来对 #{} 进行处理,处理方法在解析器中的 parse 进行处理,最后将解析出来的 ParameterMappings 最为参数传进 StaticSqlSource 对象进行返回。

下面来看看 parse 方法

org.apache.ibatis.parsing.GenericTokenParser#parse

public String parse(String text) {// 忽略代码// 如果碰到 #{} 字样的字符串 则会调用 下面方法进行解析handler.handleToken(expression.toString());// 忽略代码// 返回解析完成的 sqlreturn builder.toString();
}

下面来看看 handleToken 方法。

org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#handleToken

public String handleToken(String content) {// 解析parameterMappings.add(buildParameterMapping(content));// 返回 ? 号替换原来的字符串return "?";
}

org.apache.ibatis.builder.SqlSourceBuilder.ParameterMappingTokenHandler#buildParameterMapping

/** 将#{xxx} 占位符中的内容解析成 Map。*   #{age,javaType=int,jdbcType=NUMERIC,typeHandler=MyTypeHandler}*      上面占位符中的内容最终会被解析成如下的结果:*  {*      "property": "age",*      "typeHandler": "MyTypeHandler", *      "jdbcType": "NUMERIC", *      "javaType": "int"*  }*/
private ParameterMapping buildParameterMapping(String content) {Map propertiesMap = parseParameterMapping(content);String property = propertiesMap.get("property");Class propertyType;// metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象if (metaParameters.hasGetter(property)) { // issue #448 get type from additional paramspropertyType = metaParameters.getGetterType(property);/** parameterType 是运行时参数的类型。如果用户传入的是单个参数,比如 Employe 对象,此时 * parameterType 为 Employe.class。如果用户传入的多个参数,比如 [id = 1, author = "chenhao"],* MyBatis 会使用 ParamMap 封装这些参数,此时 parameterType 为 ParamMap.class。*/} else if (typeHandlerRegistry.hasTypeHandler(parameterType)) {propertyType = parameterType;} else if (JdbcType.CURSOR.name().equals(propertiesMap.get("jdbcType"))) {propertyType = java.sql.ResultSet.class;} else if (property == null || Map.class.isAssignableFrom(parameterType)) {propertyType = Object.class;} else {/** 代码逻辑走到此分支中,表明 parameterType 是一个自定义的类,* 比如 Employe,此时为该类创建一个元信息对象*/MetaClass metaClass = MetaClass.forClass(parameterType, configuration.getReflectorFactory());// 检测参数对象有没有与 property 想对应的 getter 方法if (metaClass.hasGetter(property)) {// 获取成员变量的类型propertyType = metaClass.getGetterType(property);} else {propertyType = Object.class;}}ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType);// 将 propertyType 赋值给 javaTypeClass javaType = propertyType;String typeHandlerAlias = null;// 遍历 propertiesMapfor (Map.Entry entry : propertiesMap.entrySet()) {String name = entry.getKey();String value = entry.getValue();if ("javaType".equals(name)) {// 如果用户明确配置了 javaType,则以用户的配置为准javaType = resolveClass(value);builder.javaType(javaType);} else if ("jdbcType".equals(name)) {// 解析 jdbcTypebuilder.jdbcType(resolveJdbcType(value));} else if ("mode".equals(name)) {builder.mode(resolveParameterMode(value));} else if ("numericScale".equals(name)) {builder.numericScale(Integer.valueOf(value));} else if ("resultMap".equals(name)) {builder.resultMapId(value);} else if ("typeHandler".equals(name)) {typeHandlerAlias = value;} else if ("jdbcTypeName".equals(name)) {builder.jdbcTypeName(value);} else if ("property".equals(name)) {// Do Nothing} else if ("expression".equals(name)) {throw new BuilderException("Expression based parameters are not supported yet");} else {throw new BuilderException("An invalid property '" + name + "' was found in mapping #{" + content + "}.  Valid properties are " + parameterProperties);}}if (typeHandlerAlias != null) {builder.typeHandler(resolveTypeHandler(javaType, typeHandlerAlias));}// 构建 ParameterMapping 对象return builder.build();
}

至此,SQL 中的 #{name, …} 占位符被替换成了问号 ? ,#{name, …} 也被解析成了一个 ParameterMapping 对象。

下面来看看 StaticSqlSource 的创建过程。如下:

return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
public class StaticSqlSource implements SqlSource {private final String sql;private final List parameterMappings;private final Configuration configuration;public StaticSqlSource(Configuration configuration, String sql) {this(configuration, sql, null);}public StaticSqlSource(Configuration configuration, String sql, List parameterMappings) {this.sql = sql;this.parameterMappings = parameterMappings;this.configuration = configuration;}@Overridepublic BoundSql getBoundSql(Object parameterObject) {// 创建 BoundSql 对象return new BoundSql(configuration, sql, parameterMappings, parameterObject);}}

最后我们就可以通过调用下面方法获取到 SQL

BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

下面我们回到 query 方法。createCacheKey 方法和缓存相关,这里就先不做分析,后面再说,接着我们分析 query 方法。

1.2 query

org.apache.ibatis.executor.CachingExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)

public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {// 从 MappedStatement 中获取缓存Cache cache = ms.getCache();// 若映射文件中未配置缓存或参照缓存,此时 cache = nullif (cache != null) {flushCacheIfRequired(ms);if (ms.isUseCache() && resultHandler == null) {ensureNoOutParams(ms, boundSql);@SuppressWarnings("unchecked")List list = (List) tcm.getObject(cache, key);if (list == null) {// 若缓存未命中,则调用被装饰类的 query 方法,也就是SimpleExecutor的query方法list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}// 调用被装饰类的 query 方法,这里的delegate我们知道应该是SimpleExecutorreturn delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

上面的代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方法。被装饰类为SimpleExecutor,而SimpleExecutor继承BaseExecutor,那我们来看看 BaseExecutor 的query方法。

org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler, org.apache.ibatis.cache.CacheKey, org.apache.ibatis.mapping.BoundSql)

public  List 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 list;try {queryStack++;// 从一级缓存中获取缓存项,一级缓存我们也下一节单独讲list = resultHandler == null ? (List) 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 #601deferredLoads.clear();if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;
}

从一级缓存中查找查询结果。若缓存未命中,再向数据库进行查询。至此我们明白了一级二级缓存的大概思路,先从二级缓存中查找,若未命中二级缓存,再从一级缓存中查找,若未命中一级缓存,再从数据库查询数据,那我们来看看是怎么从数据库查询的

org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

private  List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {List list;// 向缓存中存储一个占位符localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 调用 doQuery 进行查询list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// 移除占位符localCache.removeObject(key);}// 缓存查询结果localCache.putObject(key, list);if (ms.getStatementType() == StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;
}

调用了doQuery方法进行查询,最后将查询结果放入一级缓存。

org.apache.ibatis.executor.SimpleExecutor#doQuery

public  List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt = null;try {Configuration configuration = ms.getConfiguration();// 创建 StatementHandlerStatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 创建 Statementstmt = prepareStatement(handler, ms.getStatementLog());// 执行查询操作return handler.query(stmt, resultHandler);} finally {// 关闭 StatementcloseStatement(stmt);}
}

该方法执行分为三步:

  • 创建StatementHandler
  • 创建 Statement
  • 执行 query

1.2.1 创建 StatementHandler

org.apache.ibatis.session.Configuration#newStatementHandler

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 创建具有路由功能的 StatementHandlerStatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 应用插件到 StatementHandler 上statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;
}

我们看看RoutingStatementHandler的构造方法

org.apache.ibatis.executor.statement.RoutingStatementHandler#RoutingStatementHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// 根据 StatementType 创建不同的 StatementHandler switch (ms.getStatementType()) {case STATEMENT:delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException("Unknown statement type: " + ms.getStatementType());}}

RoutingStatementHandler 的构造方法会根据 MappedStatement 中的 statementType 变量创建不同的 StatementHandler 实现类。那statementType 是什么呢?我们还要回顾一下MappedStatement 的创建过程

public final class MappedStatement {public static class Builder {private MappedStatement mappedStatement = new MappedStatement();public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {mappedStatement.configuration = configuration;mappedStatement.id = id;mappedStatement.sqlSource = sqlSource;mappedStatement.statementType = StatementType.PREPARED;mappedStatement.parameterMap = new ParameterMap.Builder(configuration, "defaultParameterMap", null, new ArrayList()).build();mappedStatement.resultMaps = new ArrayList();mappedStatement.sqlCommandType = sqlCommandType;mappedStatement.keyGenerator = configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;String logId = id;if (configuration.getLogPrefix() != null) {logId = configuration.getLogPrefix() + id;}mappedStatement.statementLog = LogFactory.getLog(logId);mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();}}
}

我们看到 statementType 的默认类型为PREPARED,这里将会创建PreparedStatementHandler,接着往下分析。

1.2.2 创建 Statement

stmt = prepareStatement(handler, ms.getStatementLog());

org.apache.ibatis.executor.SimpleExecutor#prepareStatement

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {Statement stmt;// 获取数据库连接Connection connection = getConnection(statementLog);// 创建 Statement,stmt = handler.prepare(connection, transaction.getTimeout());// 为 Statement 设置参数handler.parameterize(stmt);return stmt;
}

在上面的代码中我们终于看到了和 jdbc 相关的内容了,上述方法分三个步骤:

  1. 获取数据库连接
  2. 创建 PreparedStatement
  3. 为 PreparedStatement 设置运行时参数

1)获取数据库连接

org.apache.ibatis.executor.BaseExecutor#getConnection

protected Connection getConnection(Log statementLog) throws SQLException {//通过transaction来获取ConnectionConnection connection = transaction.getConnection();if (statementLog.isDebugEnabled()) {return ConnectionLogger.newInstance(connection, statementLog, queryStack);} else {return connection;}
}

该方法通过 Executor 中的 transaction 属性来获取 Connection 。

由 MyBatis 配置文件中的 配置可知,MyBatis 会创建一个 JdbcTransactionFactory 类型实例来创建 JdbcTransaction 实例赋值给 transaction 属性。

那,先来看看 Transaction 接口。

public interface Transaction {//获取数据库连接Connection getConnection() throws SQLException;//提交事务void commit() throws SQLException;//回滚事务void rollback() throws SQLException;//关闭事务void close() throws SQLException;//获取超时时间Integer getTimeout() throws SQLException;
}

接着我们看看其实现类JdbcTransaction

public class JdbcTransaction implements Transaction {private static final Log log = LogFactory.getLog(JdbcTransaction.class);//数据库连接protected Connection connection;//数据源信息protected DataSource dataSource;//隔离级别protected TransactionIsolationLevel level;//是否为自动提交protected boolean autoCommmit;public JdbcTransaction(DataSource ds, TransactionIsolationLevel desiredLevel, boolean desiredAutoCommit) {dataSource = ds;level = desiredLevel;autoCommmit = desiredAutoCommit;}public JdbcTransaction(Connection connection) {this.connection = connection;}@Overridepublic Connection getConnection() throws SQLException {//如果事务中不存在connection,则获取一个connection并放入connection属性中//第一次肯定为空if (connection == null) {openConnection();}//如果事务中已经存在connection,则直接返回这个connectionreturn connection;}/*** commit()功能 * @throws SQLException*/@Overridepublic void commit() throws SQLException {if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Committing JDBC Connection [" + connection + "]");}//使用connection的commit()connection.commit();}}/*** rollback()功能 * @throws SQLException*/@Overridepublic void rollback() throws SQLException {if (connection != null && !connection.getAutoCommit()) {if (log.isDebugEnabled()) {log.debug("Rolling back JDBC Connection [" + connection + "]");}//使用connection的rollback()connection.rollback();}}/*** close()功能 * @throws SQLException*/@Overridepublic void close() throws SQLException {if (connection != null) {resetAutoCommit();if (log.isDebugEnabled()) {log.debug("Closing JDBC Connection [" + connection + "]");}//使用connection的close()connection.close();}}protected void setDesiredAutoCommit(boolean desiredAutoCommit) {try {if (connection.getAutoCommit() != desiredAutoCommit) {if (log.isDebugEnabled()) {log.debug("Setting autocommit to " + desiredAutoCommit + " on JDBC Connection [" + connection + "]");}connection.setAutoCommit(desiredAutoCommit);}} catch (SQLException e) {// Only a very poorly implemented driver would fail here,// and there's not much we can do about that.throw new TransactionException("Error configuring AutoCommit.  "+ "Your driver may not support getAutoCommit() or setAutoCommit(). "+ "Requested setting: " + desiredAutoCommit + ".  Cause: " + e, e);}}protected void resetAutoCommit() {try {if (!connection.getAutoCommit()) {// MyBatis does not call commit/rollback on a connection if just selects were performed.// Some databases start transactions with select statements// and they mandate a commit/rollback before closing the connection.// A workaround is setting the autocommit to true before closing the connection.// Sybase throws an exception here.if (log.isDebugEnabled()) {log.debug("Resetting autocommit to true on JDBC Connection [" + connection + "]");}//通过connection设置事务是否自动提交connection.setAutoCommit(true);}} catch (SQLException e) {if (log.isDebugEnabled()) {log.debug("Error resetting autocommit to true "+ "before closing the connection.  Cause: " + e);}}}protected void openConnection() throws SQLException {if (log.isDebugEnabled()) {log.debug("Opening JDBC Connection");}//通过dataSource来获取connection,并设置到transaction的connection属性中connection = dataSource.getConnection();if (level != null) {//通过connection设置事务的隔离级别connection.setTransactionIsolation(level.getLevel());}//设置事务是否自动提交setDesiredAutoCommit(autoCommmit);}@Overridepublic Integer getTimeout() throws SQLException {return null;}}

我们看到 JdbcTransaction 中有一个 Connection 属性和 dataSource 属性,使用 connection 来进行提交、回滚、关闭等操作,也就是说 JdbcTransaction 其实只是在 jdbc 的 connection 上面封装了一下,实际使用的其实还是jdbc的事务。

现在,我们来看看 getConnection() 方法。

org.apache.ibatis.transaction.jdbc.JdbcTransaction#getConnection

public Connection getConnection() throws SQLException {//如果事务中不存在connection,则获取一个connection并放入connection属性中//第一次肯定为空if (connection == null) {openConnection();}//如果事务中已经存在connection,则直接返回这个connectionreturn connection;
}

org.apache.ibatis.transaction.jdbc.JdbcTransaction#openConnection

protected void openConnection() throws SQLException {if (log.isDebugEnabled()) {log.debug("Opening JDBC Connection");}//通过dataSource来获取connection,并设置到transaction的connection属性中connection = dataSource.getConnection();if (level != null) {//通过connection设置事务的隔离级别connection.setTransactionIsolation(level.getLevel());}//设置事务是否自动提交setDesiredAutoCommit(autoCommmit);
}

先是判断当前事务中是否存在connection,如果存在,则直接返回connection,如果不存在则通过dataSource来获取connection。

这里我们明白了一点,如果当前事务没有关闭,也就是没有释放connection,那么在同一个Transaction中使用的是同一个connection。

我们再来想想,transaction是SimpleExecutor中的属性,SimpleExecutor又是SqlSession中的属性,那我们可以这样说,同一个SqlSession中只有一个SimpleExecutor,SimpleExecutor中有一个Transaction,Transaction有一个connection。

我们再来看看如下例子:

public static void main(String[] args) throws IOException {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);//创建一个SqlSessionSqlSession sqlSession = sqlSessionFactory.openSession();try {EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class);UserMapper userMapper = sqlSession.getMapper(User.class);List allEmployee = employeeMapper.getAll();List allUser = userMapper.getAll();Employee employee = employeeMapper.getOne();} finally {sqlSession.close();}
}

我们可以看到一个 sqlSession 可以获取多个 Mapper 代理对象,这些 Mapper 代理对象中的 Connection 的值肯定是同一个连接,直到调用 close() 。所以我们的 sqlSession 是线程不安全的,如果所有的业务都使用一个 sqlSession,那 Connection 也是同一个,如果一个业务执行完了就将其关闭,那其他还没执行完的业务则会出现问题。

我们回归到源码:

connection = dataSource.getConnection();

最终还是调用 dataSource 来获取连接,那我们是不是要来看看dataSource呢?

我们还是从前面的配置文件来看,这里有UNPOOLED和POOLED两种DataSource。

  • UNPOOLED:非池化的 DataSource,将会创将new UnpooledDataSource()实例
  • POOLED:池化的 DataSource,将会new pooledDataSource()实例

UnpooledDataSource 和 PooledDataSource 都实现 DataSource 接口,那我们先来看看DataSource接口。

public interface DataSource  extends CommonDataSource, Wrapper {//获取数据库连接Connection getConnection() throws SQLException;//获取数据库连接Connection getConnection(String username, String password) throws SQLException;
}  

很简单,只有一个获取数据库连接的接口,那我们来看看其实现类

UnpooledDataSource

UnpooledDataSource,从名称上即可知道,该种数据源不具有池化特性。该种数据源每次会返回一个新的数据库连接,而非复用旧的连接。其核心的方法有三个,分别如下:

  • initializeDriver - 初始化数据库驱动
  • doGetConnection - 获取数据连接
  • configureConnection - 配置数据库连接

PooledDataSource

PooledDataSource 内部实现了连接池功能,用于复用数据库连接。因此,从效率上来说,PooledDataSource 要高于 UnpooledDataSource。但是最终获取Connection还是通过UnpooledDataSource,只不过PooledDataSource 提供一个存储Connection的功能。

在这里,我只是简单的提了一下 UnpooledDataSource、PooledDataSource 这两个类,并没有深入其中的源码,不过其源码的大致执行流程还是非常容易看懂的,如果大家感兴趣可以自己去看看。

2)创建PreparedStatement

下面我们回到 doQuery 方法。

org.apache.ibatis.executor.SimpleExecutor#doQuery

stmt = handler.prepare(connection, transaction.getTimeout());

org.apache.ibatis.executor.statement.BaseStatementHandler#prepare

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {ErrorContext.instance().sql(boundSql.getSql());Statement statement = null;try {// 创建 Statementstatement = instantiateStatement(connection);// 设置超时和 FetchSizesetStatementTimeout(statement, transactionTimeout);setFetchSize(statement);return statement;} catch (SQLException e) {closeStatement(statement);throw e;} catch (Exception e) {closeStatement(statement);throw new ExecutorException("Error preparing statement.  Cause: " + e, e);}
}

org.apache.ibatis.executor.statement.PreparedStatementHandler#instantiateStatement

protected Statement instantiateStatement(Connection connection) throws SQLException {//获取sql字符串,比如"select * from user where id= ?"String sql = boundSql.getSql();// 根据条件调用不同的 prepareStatement 方法创建 PreparedStatementif (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {String[] keyColumnNames = mappedStatement.getKeyColumns();if (keyColumnNames == null) {//通过connection获取Statement,将sql语句传进去return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);} else {return connection.prepareStatement(sql, keyColumnNames);}} else if (mappedStatement.getResultSetType() != null) {return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);} else {return connection.prepareStatement(sql);}
}

看到没和jdbc的形式一模一样,通过 connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS) 方法的调用,我们的 Statement 对象就生成完毕了。

3)为 PreparedStatement 设置运行时参数

下面我们回到 doQuery 方法。

org.apache.ibatis.executor.SimpleExecutor#doQuery

handler.parameterize(stmt);

org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize

public void parameterize(Statement statement) throws SQLException {// 通过参数处理器 ParameterHandler 设置运行时参数到 PreparedStatement 中parameterHandler.setParameters((PreparedStatement) statement);
}

org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters

public void setParameters(PreparedStatement ps) {ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());/** 从 BoundSql 中获取 ParameterMapping 列表,每个 ParameterMapping 与原始 SQL 中的 #{xxx} 占位符一一对应*/List parameterMappings = boundSql.getParameterMappings();if (parameterMappings != null) {for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping parameterMapping = parameterMappings.get(i);if (parameterMapping.getMode() != ParameterMode.OUT) {Object value;// 获取属性名String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional paramsvalue = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {value = parameterObject;} else {// 为用户传入的参数 parameterObject 创建元信息对象MetaObject metaObject = configuration.newMetaObject(parameterObject);// 从用户传入的参数中获取 propertyName 对应的值value = metaObject.getValue(propertyName);}TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 由类型处理器 typeHandler 向 ParameterHandler 设置参数typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);} catch (SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}
}

该方法的本意我相信大家都清楚,就是给 SQL 中的 ? 赋值,那我们来看看 setParameters 方法是怎么做的。

首先从 boundSql 中获取 parameterMappings 集合,然后遍历获取 parameterMapping 中的 propertyName ,如#{name} 中的name,然后从运行时参数parameterObject中获取 name 对应的参数值,最后设置到PreparedStatement 中。

我们主要来看是如何设置参数的,也就是 typeHandler.setParameter(ps, i + 1, value, jdbcType) 这句代码。

分析设置参数之前,先来看看 TypeHandler 接口。该接口是参数设置的抽象定义,具体实现逻辑由其子类实现。如:StringTypeHandler、IntegerTypeHandler、BooleanTypeHandler等。

下面我们以 StringTypeHandler 为例子,看看其如何执行的。

org.apache.ibatis.type.StringTypeHandler#setNonNullParameter

public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)throws SQLException {ps.setString(i, parameter);
}

setString 方法就不进去看了,已经涉及到很底层的源码了,它的本质就是字符串替换,但替换前做了很多安全性的检查。

1.2.3 执行 query

回到 doQuery 方法,来看看最后一步查询。

org.apache.ibatis.executor.SimpleExecutor#doQuery

return handler.query(stmt, resultHandler);

org.apache.ibatis.executor.statement.PreparedStatementHandler#query

public  List query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps = (PreparedStatement) statement;//直接执行ServerPreparedStatement的execute方法ps.execute();//进行resultSet自动映射return resultSetHandler. handleResultSets(ps);
}

该方法主要有两个步骤:

  • 执行 SQL
  • 解析返回结果

对于执行 SQL 非常简单,直接调用 PreparedStatement 对象的 execute 方法即可完成 SQL 的执行。

下面我们看看执行后的结果集是如何处理的。

好了,今天的内容到这里就结束了,我是 【J3】关注我,我们下期见


  • 由于博主才疏学浅,难免会有纰漏,假如你发现了错误或偏见的地方,还望留言给我指出来,我会对其加以修正。

  • 如果你觉得文章还不错,你的转发、分享、点赞、留言就是对我最大的鼓励。

  • 感谢您的阅读,十分欢迎并感谢您的关注。

相关内容

热门资讯

北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...