在《Mybatis SQL执行流程(一)》中我们已经通过SqlSession的getMapper()方法获得了Mapper接口的代理对象,此时就可以直接通过调用代理对象的方法来执行SQL语句了,具体又是怎么执行的呢?这一节将重点介绍SQL的执行流程
Mapper接口代理对象对应的InvocationHandler实现类是MapperProxy,所以当调用接口的方法时,首先就会进入到MapperProxy的invoke()方法中,我们先看下MapperProxy的invoke()方法实现
在方法调用时,如果是调用Object的方法或者接口中的默认方法,直接通过反射调用
调用接口中的其他方法,才会去走执行SQL的逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 判断我们的方法是不是我们的Object类定义的方法,若是直接通过反射调用
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
} else if (method.isDefault()) { //是否接口的默认方法
// 调用我们的接口中的默认方法
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
// 真正的进行调用,做了二个事情
// 第一步:把我们的方法对象封装成一个MapperMethod对象(带有缓存作用的)
final MapperMethod mapperMethod = cachedMapperMethod(method);
//
return mapperMethod.execute(sqlSession, args);
}
一、 创建并缓存MapperMethod
执行SQL前,第一步就是去把调用的Method对象封装成一个MapperMethod,然后进行缓存
private MapperMethod cachedMapperMethod(Method method) {
/**
* 相当于这句代码.jdk8的新写法
* if(methodCache.get(method)==null){
* methodCache.put(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()))
* }
*/
return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
下面看一下MapperMethod里面都有哪些内容
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
// 创建我们的SqlCommand对象
this.command = new SqlCommand(config, mapperInterface, method);
// 创建我们的方法签名对象
this.method = new MethodSignature(config, mapperInterface, method);
}
其中SqlCommand对象里面会存接口的全限定名和接口对应的SQL操作类型
public static class SqlCommand {
// 接口的方法名全路径比如:com.lizhi.mapper.UserMapper.selectById
private final String name;
// 对应接口方法操作的sql类型(是insert|update|delte|select)
private final SqlCommandType type;
}
而MethodSignature方法签名中存了方法的返回值类型和参数解析器,参数解析器初始的时候,就回去解析是方法的方法,然后按照参数下标索引存放在ParamNameResolver的names属性中,这是一个SortedMap,在参数解析的时候,会过滤掉逻辑分页RowBounds类型的参数和ResultHandler类型的参数
public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
// 解析方法的返回值类型
Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
//判断返回值是不是class类型的
if (resolvedReturnType instanceof Class<?>) {
this.returnType = (Class<?>) resolvedReturnType;
} else if (resolvedReturnType instanceof ParameterizedType) {
//是不是参数泛型的
this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
} else {
//普通的
this.returnType = method.getReturnType();
}
//返回值是不是为空
this.returnsVoid = void.class.equals(this.returnType);
//返回是是不是集合类型
this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
//返回值是不是游标
this.returnsCursor = Cursor.class.equals(this.returnType);
//返回值是不是optionnal类型的
this.returnsOptional = Optional.class.equals(this.returnType);
// 初始化我们参数解析器对象
this.paramNameResolver = new ParamNameResolver(configuration, method);
}
二、根据操作类型进行调用
得到MapperMethod对象后,就去调用它的execute()方法来执行SQL
在execute()方法中,会根据方法对应的SQL操作类型,执行对应的方法。
所有的操作最后都换转换成去调用SqlSession中的方法
我们以调用UseMapper的selectById()方法为例,它的返回值是一个User对象,所以会去调用SELECT
下的最后一个分支,下面具体看一下执行逻辑
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 判断我们执行sql命令的类型
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
……
}
case DELETE: {
……
}
case SELECT:
//返回值为空
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
//返回值是一个List
result = executeForMany(sqlSession, args);
}
……
else {
//查询返回单个
// 解析我们的参数
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
……
}
return result;
}
2.1 参数解析
参数解析时,会利用MethodSignature中参数解析器的getNamedParams()方法进行解析
Object param = method.convertArgsToSqlCommandParam(args);
names
属性就是参数解析器实例化的时候,解析方法参数得到的一个SortMap,其中KEY为参数的索引下标,VALUE为参数名
如果只有一个没有加@Param注解的参数,就直接返回该参数值
其他有参数的情况,会把这些参数全部封装到一个Map里面,然后返回整个Map对象
public Object getNamedParams(Object[] args) {
//获取参数的个数
// names的数据结构为map ({key="0",value="id"},{key="1",value="name"})
final int paramCount = names.size();
//若参数的个数为空或者个数为0直接返回
if (args == null || paramCount == 0) {
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
// 若有且只有一个参数 而且没有标注了@Param指定方法方法名称
return args[names.firstKey()];
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
// 循坏我们所有的参数的个数
for (Map.Entry<Integer, String> entry : names.entrySet()) {
//把key为id,value为1加入到param中
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
//加入通用的参数:名称为param+0,1,2,3......
final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
//把key为param+0,1,2,3.....,value值加入到param中
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
2.2 调用SqlSession方法
参数解析完成之后,就会去调用SqlSession的方法了,我们看SqlSession的selectOne()方法
public <T> T selectOne(String statement, Object parameter) {
// 这里selectOne调用也是调用selectList方法
List<T> list = this.selectList(statement, parameter);
//若查询出来有且有一个一个对象,直接返回要给
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
// 查询的有多个,那么就抛出我们熟悉的异常
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
在具体的SelectList()方法中的,会根据方法名拿到Configuration中的MappedStatement对象,这个MappedStatement对象就是在解析SQL的xml文件,把每一个SQL最终解析成了一个MappedStatement对象,具体的解析过程可以在《Mybatis 配置文件解析(二)》中查看,然后去调用Executor实例的query()方法
文章前面介绍了Executor对象的创建,如果开启了缓存,会生成一个CachingExecutor实例封装到SqlSession中,这里我们开启了缓存,所以会去执行CachingExecutor的query()方法
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
// 第一步:通过我们的statement去我们的全局配置类中获取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
/**
* 通过执行器去执行我们的sql对象
* 第一步:包装我们的集合类参数
* 第二步:一般情况下是executor为cacheExetory对象
*/
return executor.query(ms, wrapCollection(parameter), rowBounds,
}
2.3 解析动态SQL
在Executor的query()方法中,首先会去调用MappedStatement的getBoundSql()方法对动态SQL进行解析,在SQL解析的时候,只是按照if
、where
等这些标签把它们解析成了一个个的SqlNode,因为不知道参数值,所以不知道哪些节点需要保留
而getBoundSql()方法会把方法请求的参数传进去,有了参数值,就可以对这些SqlNode进行解析,最后生成一个完成的SQL
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
/**
* 通过参数对象解析我们的sql详细信息1339025938:1570540512:com.lizhi.mapper.selectById:0:2147483647:select id,user_name,create_time from t_user where id=?:1:development
*/
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
2.3.1 拼接SQL
在getBoundSql()方法中,根据SQL配置解析时生成的SqlSource类型,调用它的getBoundSql()方法,动态SQL对应DynamicSqlSource
public BoundSql getBoundSql(Object parameterObject) {
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
……
}
rootSqlNode为SqlSource的根节点,是一个MixedSqlNode,会去调用它的apply()方法
MixedSqlNode下面会有多个SqlNode,就回去遍历所有SqlNode的apply()方法
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 1归 责任链 处理一个个SqlNode 编译出一个完整sql
rootSqlNode.apply(context);
……
}
public boolean apply(DynamicContext context) {
contents.forEach(node -> node.apply(context));
return true;
}
下面我们以if
标签生成的SqlNode为例
在if
对应的SqlNode中,会判断它的test
表达式是否为true,如果为true,会去调用它下面的SqlNode(and id=#{id}
),这SqlNode是一个TextSqlNode
public boolean apply(DynamicContext context) {
if (evaluator.evaluateBoolean(test, context.getBindings())) {
// 处理完<if 递归回去继续处理
contents.apply(context);
return true;
}
return false;
}
在TextSqlNode中,会调用解析器的parse()方法判断方法传进来的参数中是否有这么一个名字的参数,最后把TextSqlNode对应的内容通过appedSql()方法进行拼接
public boolean apply(DynamicContext context) {
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
}
public void appendSql(String sql) {
sqlBuilder.add(sql);
}
private final StringJoiner sqlBuilder = new StringJoiner(" ");
注:所有if
、where
等对应的动态节点,它们最底层的节点一定是TextSqlNode或者StaticTextSqlNode,会根据前面节点解析表达式的结果来决定是否拼接TextSqlNode或StaticTextSqlNode的内容
2.3.2 替换参数
在getBoundSql()方法中,将SqlNode拼接成完整的SQL语句之后,就会调用SqlSource解析器来解析SQL中的#{}
占位符
public BoundSql getBoundSql(Object parameterObject) {
DynamicContext context = new DynamicContext(configuration, parameterObject);
// 1归 责任链 处理一个个SqlNode 编译出一个完整sql
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
// 2.接下来处理 处理sql中的#{...}
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
// 怎么处理呢? 很简单, 就是拿到#{}中的内容 封装为parameterMapper, 替换成?
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
context.getBindings().forEach(boundSql::setAdditionalParameter);
return boundSql;
}
在SqlSourceBuilder的parse()方法中,首先会创建一个ParameterMappingTokenHandler类型的处理器,它负责把#{}
占位符里面的参数解析成一个ParameterMapping对象,这一步是在ParameterMappingTokenHandler处理器的parse()方法中完成,调用parse()同时会把#{}
替换成?
占位符,然后返回替换后的SQL语句,此时的SQL语句与原生JDBC的SQL就没什么两样了,然后封装成一个新的StaticSqlSource对象
然后在调用getBoundSql把方法参数也设置进去,得到一个新的BoundSql对象并返回
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
// 替换sql中的#{} 替换成问号, 并且会顺便拿到#{}中的参数名解析成ParameterMapping
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}
2.4 执行查询
2.4.1 查询二级缓存
我们再回到最前面CachingExecutor的query()方法,前面我们已经根据参数把动态SQL解析成了一个静态的SQL了,接下来会根据SQL的信息生成一个二级缓存对应的KEY–CacheKey实例,这个CachKey的id
就是SQL对应的Mapper接口的全限定名
最后就是去调用重载的query()方法进行查询
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameterObject);
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
这个方法的功能就很明显了,因为当前的Executor是一个CachingExecutor,所以它会去判断是否开启了二级缓存。如果开启了二级缓存,就先从二级缓存里面拿,拿不到再去查询数据库,最后再把查询出来的结果放入到二级缓存中;如果没有开启二级缓存,就直接去查询数据库
其中delegate
属性就是被CachingExecutor包装的SimpleExecutor|ReuseExecutor|BatchExecutor中的某一个
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
throws SQLException {
// 判断我们我们的mapper中是否开启了二级缓存<cache></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);
}
2.4.2 查询数据库
在基本Executor实例的query()方法中,首先会去一级缓存中去拿,一级缓存没有才会去真正查询数据库,从localCache
这个成员变量也可以看出来,它位于Executor中,意味着它只是SqlSession级别的缓存,当SqlSession提交或关闭时,它就失效了
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());
//已经关闭,则抛出 ExecutorException 异常
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// <2> 清空本地缓存,如果 queryStack 为零,并且要求清空本地缓存。
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
// <4.1> 从一级缓存中,获取查询结果
queryStack++;
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
// <4.2> 获取到,则进行处理
if (list != null) {
//处理存过的
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 获得不到,则从数据库中查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
……
return list;
}
在queryFromDatabase()方法去调用doQuery()方法执行SQL,以SimpleExecutor为准来看一下具体实现
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
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;
}
首先创建一个StatementHandler的实例,在newStatementHandler()方法中,会去处理Statement类型的拦截器,与处理Executor类型的拦截器逻辑一样,不复述了
然后在prepareStatement()方法中,会去获取数据库连接,创建statement对象并进行预处理,最后处理SQL语句的参数
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 handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 拿到连接和statement
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
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;
}
根据参数类型拿到对应的参数类型处理器,调用参数处理器的setParameter()方法来设置参数
以selectByid(int id)方法为例,会去调用IntegerTypeHandler的setNonNullParameter()方法
最后会去调用setNonNullParameter()方法设置参数,这就回到了我们数据的JDBC操作数据库的写法了
public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
throws SQLException {
ps.setInt(i, parameter);
}
最后调用SimpleStatementHandler的query()方法,执行SQL,处理结果集
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}