Mybatis SQL执行流程(二)

《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解析的时候,只是按照ifwhere等这些标签把它们解析成了一个个的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(" ");

注:所有ifwhere等对应的动态节点,它们最底层的节点一定是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);
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值