Mybatis源码分析(6)之SQL执行流程

一、前言

        我们进行一个mybatis框架的基础代码可以如下:

    @Test
    public void test2() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        SqlSession sqlSession = sqlSessionFactory.openSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.selectUser(1);
        log.info("user:{}", user);

    }

        在前面的章节中我们已经介绍了SqlSessionFactoryBuilder的build方法去说明如何解析我们的配置文件生成Configuration对象,而要执行对应的sql我们就要创建会话sqlSession。

二、会话创建源码解析

        首先我们还是先来了解一下会话对象的组成结构如下图:

Mybatis源码分析(6)之SQL执行流程_第1张图片

            上面是会话的对象结构图,接下来我们将对他的创建过程的源码进行分析,剖析为啥会是这样子的结构。

2.1openSession源码解析

        首先我们通过DefaultSqlSessionFactory的openSession来打开一个会话,具体源码如下:

    public SqlSession openSession() {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }


  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // 获取Mybatis主配置文件配置的环境信息
      final Environment environment = configuration.getEnvironment();
      // 创建事务管理器工厂
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 创建事务管理器
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 根据Mybatis主配置文件中指定的Executor类型创建对应的Executor实例
      final Executor executor = configuration.newExecutor(tx, execType);
      // 创建DefaultSqlSession实例
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

        上面的逻辑也比较简单,首先获取Mybatis主配置文件配置的环境信息,然后创建事务管理器工厂以及创建事务管理器,然后根据Mybatis主配置文件中指定的Executor类型创建对应的Executor实例,mybatis有默认指定的defaultExecutorType,默认为ExecutorType.SIMPLE;

2.2 newExecutor源码分析

        创建newExecutor实例的源码具体如下:

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        // 根据executor类型创建对象的Executor对象
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
        // 如果cacheEnabled属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        // 执行拦截器链的拦截逻辑
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }

        首先会根据根据executor类型创建对象的Executor对象,如果cacheEnabled属性为ture,这使用CachingExecutor对上面创建的Executor进行装饰,然后执行拦截器链的拦截逻辑。

三、 SQL 执行入口分析

        在单独使用 MyBatis 进行数据库操作时,我们通常都会先调用 SqlSession 接口的 getMapper 方法为我们的 Mapper 接口生成实现类。然后就可以通过 Mapper 进行数据库操作。比如像下面这样:

        com.jiagouedu.mapper.UserMapper mapper = sqlSession.getMapper(com.jiagouedu.mapper.UserMapper.class);
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        criteria.andIdEqualTo(1L);
        List userList = mapper.selectByExample(userExample);
        log.info("user:{}", userList.get(0));

        如果大家对 MyBatis 较为理解,会知道 SqlSession 是通过 JDK 动态代理的方式为接口生成代理对象的。在调用接口方法时,方法调用会被代理逻辑拦截。在代理逻辑中可根据方法名及方法归属接口获取到当前方法对应的 SQL 以及其他一些信息,拿到这些信息即可进行数据库操作。

2.1为 Mapper 接口创建代理对象

        本节,我们从 DefaultSqlSession 的 getMapper 方法开始看起,如下:

    // DefaultSqlSession    
    public  T getMapper(Class type) {
        return configuration.getMapper(type, this);
    }

    // Configuration
    public  T getMapper(Class type, SqlSession sqlSession) {
        return mapperRegistry.getMapper(type, sqlSession);
    }




    /**
     * 根据Mapper接口Class对象获取Mapper动态代理对象
     * @author maoqichuan
     * @date 2021/12/28 15:11
     * @return T
     */
    // MapperRegistry
    @SuppressWarnings("unchecked")
    public  T getMapper(Class type, SqlSession sqlSession) {
        // 从 knownMappers 中获取与 type 对应的 MapperProxyFactor
        final MapperProxyFactory mapperProxyFactory = (MapperProxyFactory) knownMappers.get(type);
        if (mapperProxyFactory == null) {
            throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
            // 创建代理对象
            return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
            throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
    }

            大家可能不知道 knownMappers 集合中的元素是何时存入的。这里再说一遍吧,MyBatis 在解析配置文件的 节点的过程中,会调用 MapperRegistry 的 addMapper 方法将 Class 到 MapperProxyFactory 对象的映射关系存入到 knownMappers。具体的代码就不分析了。

        在获取到 MapperProxyFactory 对象后,即可调用工厂方法为 Mapper 接口生成代理对象了。相关逻辑如下:

  public T newInstance(SqlSession sqlSession) {
    /*
     * 创建 MapperProxy 对象,MapperProxy 实现了
     * InvocationHandler 接口,代理逻辑封装在此类中
     */
    final MapperProxy mapperProxy = new MapperProxy(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy mapperProxy) {
    // 通过 JDK 动态代理创建代理对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

      上面的代码首先创建了一个 MapperProxy 对象,该对象实现了 InvocationHandler 接口。然后将对象作为参数传给重载方法,并在重载方法中调用 JDK 动态代理接口为 Mapper 生成代理对象。

     2.1.1执行代理逻辑

        在 MyBatis 中,Mapper 接口方法的代理逻辑实现的比较简单。该逻辑首先会对拦截的方法进行一些检测,以决定是否执行后续的数据库操作。对应的代码如下:


    /**
     * 首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、hashCode 方法等。对于这类方法,直接执行即可。除此之外,
     * MyBatis 从 3.4.2 版本开始,对 JDK 1.8 接口的默认方法提供了支持,具体就不分析了。完成相关检测后,紧接着从缓存中获取或者
     * 创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。
     * @author maoqichuan
     * @date 2021/12/28 15:20
     * @return java.lang.Object
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            // 从Object类继承的方法不做处理
            if (Object.class.equals(method.getDeclaringClass())) {
                return method.invoke(this, args);
                /*
                 * 下面的代码最早出现在 mybatis-3.4.2 版本中,用于支持 JDK 1.8 中的
                 * 新特性 - 默认方法。这段代码的逻辑就不分析了,有兴趣的同学可以
                 * 去 Github 上看一下相关的相关的讨论(issue #709),链接如下:
                 *
                 *   https://github.com/mybatis/mybatis-3/issues/709
                 */
            } else if (isDefaultMethod(method)) {
                return invokeDefaultMethod(proxy, method, args);
            }
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
        // 对Mapper接口中定义的方法进行封装,生成MapperMethod对象
        final MapperMethod mapperMethod = cachedMapperMethod(method);

        // 调用 execute 方法执行 SQL
        return mapperMethod.execute(sqlSession, args);
    }

           如上,代理逻辑会首先检测被拦截的方法是不是定义在 Object 中的,比如 equals、hashCode 方法等。对于这类方法,直接执行即可。除此之外,MyBatis 从 3.4.2 版本开始,对 JDK 1.8 接口的默认方法提供了支持,具体就不分析了。完成相关检测后,紧接着从缓存中获取或者创建 MapperMethod 对象,然后通过该对象中的 execute 方法执行 SQL。在分析 execute 方法之前,我们先来看一下 MapperMethod 对象的创建过程。MapperMethod 的创建过程看似普通,但却包含了一些重要的逻辑,所以不能忽视。

        2.1.2  创建 MapperMethod 对象

        本节来分析一下 MapperMethod 的构造方法,看看它的构造方法中都包含了哪些逻辑。如下:


/**
 * @author Clinton Begin
 * @author Eduardo Macarron
 * @author Lasse Voss
 * @author Kazuki Shimizu
 */
public class MapperMethod {

    private final SqlCommand command;
    private final MethodSignature method;

    /**
     * 主要是创建 SqlCommand 和 MethodSignature 对象。这两个对象分别记录了不同的信息,
     * 这些信息在后续的方法调用中都会被用到
     * @author maoqichuan
     * @date 2021/12/28 15:26
     */
    public MapperMethod(Class mapperInterface, Method method, Configuration config) {
        // 创建 SqlCommand 对象,该对象包含一些和 SQL 相关的信息
        this.command = new SqlCommand(config, mapperInterface, method);
        // 创建 MethodSignature 对象,从类名中可知,该对象包含了被拦截方法的一些信息
        this.method = new MethodSignature(config, mapperInterface, method);
    }
}

          如上,MapperMethod 构造方法的逻辑很简单,主要是创建 SqlCommand 和 MethodSignature 对象。这两个对象分别记录了不同的信息,这些信息在后续的方法调用中都会被用到。下面我们深入到这两个类的构造方法中,探索它们的初始化逻辑。  

        2.1.3构建SqlCommand对象

        前面说了 SqlCommand 中保存了一些和 SQL 相关的信息,那具体有哪些信息呢?答案在下面的代码中。

    public static class SqlCommand {

        private final String name; // Mapper Id
        private final SqlCommandType type; // SQL类型

        public SqlCommand(Configuration configuration, Class mapperInterface, Method method) {
            final String methodName = method.getName();
            // 获取声明该方法的类或接口的Class对象
            final Class declaringClass = method.getDeclaringClass();
            // 获取描述标签的MappedStatement对象,解析 MappedStatement
            MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
                    configuration);

            // 检测当前方法是否有对应的 MappedStatement
            if (ms == null) {
                // 检测当前方法是否有 @Flush 注解
                if (method.getAnnotation(Flush.class) != null) {
                    name = null;
                    type = SqlCommandType.FLUSH;
                } else {
                    /*
                     * 若 ms == null 且方法无 @Flush 注解,此时抛出异常。
                     * 这个异常比较常见,大家应该眼熟吧
                     */
                    throw new BindingException("Invalid bound statement (not found): "
                            + mapperInterface.getName() + "." + methodName);
                }
            } else {
                name = ms.getId();
                type = ms.getSqlCommandType();
                if (type == SqlCommandType.UNKNOWN) {
                    throw new BindingException("Unknown execution method for: " + name);
                }
            }
        }


        public String getName() {
            return name;
        }

        public SqlCommandType getType() {
            return type;
        }

        private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName,
                                                       Class declaringClass, Configuration configuration) {
            // 获取Mapper的Id
            String statementId = mapperInterface.getName() + "." + methodName;
            if (configuration.hasStatement(statementId)) {
                // 如果Configuration对象中已经注册了MappedStatement对象,则获取该MappedStatement对象
                return configuration.getMappedStatement(statementId);
            } else if (mapperInterface.equals(declaringClass)) {
                return null;
            }
            // 如果方法是在Mapper父接口中定义,则根据父接口获取对应的MappedStatement对象
            for (Class superInterface : mapperInterface.getInterfaces()) {
                if (declaringClass.isAssignableFrom(superInterface)) {
                    MappedStatement ms = resolveMappedStatement(superInterface, methodName,
                            declaringClass, configuration);
                    if (ms != null) {
                        return ms;
                    }
                }
            }
            return null;
        }
    }

        如上,SqlCommand 的构造方法主要用于初始化它的两个成员变量。代码不是很长,逻辑也不难理解,就不多说了。继续往下看。

2.1.4创建 MethodSignature 对象

        MethodSignature 即方法签名,顾名思义,该类保存了一些和目标方法相关的信息。比如目标方法的返回类型,目标方法的参数列表信息等。下面,我们来分析一下 MethodSignature 的构造方法。

public static class MethodSignature {

        private final boolean returnsMany;
        private final boolean returnsMap;
        private final boolean returnsVoid;
        private final boolean returnsCursor;
        private final boolean returnsOptional;
        private final Class returnType;
        private final String mapKey;
        private final Integer resultHandlerIndex;
        private final Integer rowBoundsIndex;
        private final ParamNameResolver paramNameResolver;

        /**
         * 用于检测目标方法的返回类型,以及解析目标方法参数列表。
         * 其中,检测返回类型的目的是为避免查询方法返回错误的类型
         * @author maoqichuan
         * @date 2021/12/28 15:38
         */
        public MethodSignature(Configuration configuration, Class mapperInterface, Method method) {
            // 获取方法返回值类型
            Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
            if (resolvedReturnType instanceof Class) {
                this.returnType = (Class) resolvedReturnType;
            } else if (resolvedReturnType instanceof ParameterizedType) {
                this.returnType = (Class) ((ParameterizedType) resolvedReturnType).getRawType();
            } else {
                this.returnType = method.getReturnType();
            }
            // 返回值类型为void
            this.returnsVoid = void.class.equals(this.returnType);
            // 返回值类型为集合
            this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
            // 返回值类型为Cursor
            this.returnsCursor = Cursor.class.equals(this.returnType);
            // 返回值类型为Optional
            this.returnsOptional = Jdk.optionalExists && Optional.class.equals(this.returnType);
            this.mapKey = getMapKey(method);
            // 返回值类型为Map
            this.returnsMap = this.mapKey != null;
            // RowBounds参数位置索引
            this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
            // ResultHandler参数位置索引
            this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
            // ParamNameResolver用于解析Mapper方法参数
            this.paramNameResolver = new ParamNameResolver(configuration, method);
        }
 // ...
}

        上面的代码用于检测目标方法的返回类型,以及解析目标方法参数列表。其中,检测返回类型的目的是为避免查询方法返回错误的类型。比如我们要求接口方法返回一个对象,结果却返回了对象集合,这会导致类型转换错误。关于返回值类型的解析过程先说到这,下面分析参数列表的解析过程。

public ParamNameResolver(Configuration config, Method method) {
        // 获取所有参数类型
        final Class[] paramTypes = method.getParameterTypes();
        // 获取所有参数注解
        final Annotation[][] paramAnnotations = method.getParameterAnnotations();
        final SortedMap map = new TreeMap();
        int paramCount = paramAnnotations.length;
        // 从@Param 注解中获取参数名称
        for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
             检测当前的参数类型是否为 RowBounds 或 ResultHandler
            if (isSpecialParameter(paramTypes[paramIndex])) {
                continue;
            }
            String name = null;
            for (Annotation annotation : paramAnnotations[paramIndex]) {
                // 方法参数中,是否有Param注解
                if (annotation instanceof Param) {
                    hasParamAnnotation = true;
                    // 获取参数名称
                    name = ((Param) annotation).value();
                    break;
                }
            }
            if (name == null) {
                // 未指定@Param 注解,这判断是否使用实际的参数名称,检测是否设置了 useActualParamName ,全局配置参考useActualParamName属性的作用
                if (config.isUseActualParamName()) {
                    // 获取参数名
                    /*
                     * 通过反射获取参数名称。此种方式要求 JDK 版本为 1.8+,
                     * 且要求编译时加入 -parameters 参数,否则获取到的参数名
                     * 仍然是 arg1, arg2, ..., argN
                     */
                    name = getActualParamName(method, paramIndex);
                }
                if (name == null) {
                    /*
                     * 使用 map.size() 返回值作为名称,思考一下为什么不这样写:
                     *   name = String.valueOf(paramIndex);
                     * 因为如果参数列表中包含 RowBounds 或 ResultHandler,这两个参数
                     * 会被忽略掉,这样将导致名称不连续。
                     * 比如参数列表 (int p1, int p2, RowBounds rb, int p3)
                     *  - 期望得到名称列表为 ["0", "1", "2"]
                     *  - 实际得到名称列表为 ["0", "1", "3"]
                     */
                    name = String.valueOf(map.size());
                }
            }
            // 將参数信息存放在Map中,Key为参数位置索引,Value为参数名称
            map.put(paramIndex, name);
        }
        // 將参数信息保存在names属性中
        names = Collections.unmodifiableSortedMap(map);
    }

        以上就是方法参数列表的解析过程,解析完毕后,可得到参数下标到参数名的映射关系,这些映射关系最终存储在 ParamNameResolver 的 names 成员变量中。这些映射关系将会在后面的代码中被用到,大家留意一下。

        2.1.5执行execute方法

        面已经分析了 MapperMethod 的初始化过程,现在 MapperMethod 创建好了。那么,接下来要做的事情是调用 MapperMethod 的 execute 方法,执行 SQL。代码如下:

    public Object execute(SqlSession sqlSession, Object[] args) {
        Object result;
        // 其中command为MapperMethod构造是创建的SqlCommand对象
        // 获取SQL语句类型
        switch (command.getType()) {
            case INSERT: {
                // 对用户传入的参数进行转换,下同
                Object param = method.convertArgsToSqlCommandParam(args);
                // 调用SqlSession的insert()方法,然后调用rowCountResult()方法统计行数
                // 执行插入操作,rowCountResult 方法用于处理返回值
                result = rowCountResult(sqlSession.insert(command.getName(), param));
                break;
            }
            case UPDATE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                // 调用SqlSession对象的update()方法
                result = rowCountResult(sqlSession.update(command.getName(), param));
                break;
            }
            case DELETE: {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = rowCountResult(sqlSession.delete(command.getName(), param));
                break;
            }
            case SELECT:
                // 根据目标方法的返回类型进行相应的查询操作
                if (method.returnsVoid() && method.hasResultHandler()) {
                    /*
                     * 如果方法返回值为 void,但参数列表中包含 ResultHandler,表明使用者
                     * 想通过 ResultHandler 的方式获取查询结果,而非通过返回值获取结果
                     */
                    executeWithResultHandler(sqlSession, args);
                    result = null;
                } else if (method.returnsMany()) {
                    // 执行查询操作,并返回多个结果
                    result = executeForMany(sqlSession, args);
                } else if (method.returnsMap()) {
                    // 执行查询操作,并将结果封装在 Map 中返回
                    result = executeForMap(sqlSession, args);
                } else if (method.returnsCursor()) {
                    // 执行查询操作,并返回一个 Cursor 对象
                    result = executeForCursor(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 = OptionalUtil.ofNullable(result);
                    }
                }
                break;
            case FLUSH:
                // 执行刷新操作
                result = sqlSession.flushStatements();
                break;
            default:
                throw new BindingException("Unknown execution method for: " + command.getName());
        }

        // 如果方法的返回值为基本类型,而返回值却为 null,此种情况下应抛出异常
        if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
            throw new BindingException("Mapper method '" + command.getName()
                    + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
        }
        return result;
    }

        如上,execute 方法主要由一个 switch 语句组成,用于根据 SQL 类型执行相应的数据库操作。该方法的逻辑清晰,不需要太多的分析。不过在上面的方法中 convertArgsToSqlCommandParam 方法出现次数比较频繁,这里分析一下:

        public Object convertArgsToSqlCommandParam(Object[] args) {
            return paramNameResolver.getNamedParams(args);
        }


    /**
     * 

* A single non-special parameter is returned without a name. * Multiple parameters are named using the naming rule. * In addition to the default names, this method also adds the generic names (param1, param2, * ...). *

*/ public Object getNamedParams(Object[] args) { final int paramCount = names.size(); if (args == null || paramCount == 0) { return null; } else if (!hasParamAnnotation && paramCount == 1) { /* * 如果方法参数列表无 @Param 注解,且仅有一个非特别参数,则返回该参数的值。 * 比如如下方法: * List findList(RowBounds rb, String name) * names 如下: * names = {1 : "0"} * 此种情况下,返回 args[names.firstKey()],即 args[1] -> name */ return args[names.firstKey()]; } else { final Map param = new ParamMap(); int i = 0; for (Map.Entry entry : names.entrySet()) { // 添加 <参数名, 参数值> 键值对到 param 中 param.put(entry.getValue(), args[entry.getKey()]); // genericParamName = param + index。比如 param1, param2, ... paramN final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1); /* * 检测 names 中是否包含 genericParamName,什么情况下会包含?答案如下: * 使用者显式将参数名称配置为 param1,即 @Param("param1") */ if (!names.containsValue(genericParamName)) { // 添加 到 param 中 param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }

        如上,convertArgsToSqlCommandParam 是一个空壳方法,该方法最终调用了 ParamNameResolver 的 getNamedParams 方法。getNamedParams 方法的主要逻辑是根据条件返回不同的结果,该方法的代码不是很难理解,我也进行了比较详细的注释,就不多说了。

        2.2查询语句执行过程分析

        查询语句对应的方法比较多,有如下几种:

  • executeWithResultHandler
  • executeForMany
  • executeForMap
  • executeForCursor

        这些方法在内部调用了 SqlSession 中的一些 select* 方法,比如 selectList、selectMap、selectCursor 等。这些方法的返回值类型是不同的,因此对于每种返回类型,需要有专门的处理方法。以 selectList 方法为例,该方法的返回值类型为 List。但如果我们的 Mapper 或 Dao 的接口方法返回值类型为数组,或者 Set,直接将 List 类型的结果返回给 Mapper/Dao 就不合适了。execute* 等方法只是对 select* 等方法做了一层简单的封装,因此接下来我们应该把目光放在这些 select* 方法上。下面我们来分析一下 selectOne 方法的源码,如下:

        2.2.1selectOne 方法分析

        本节选择分析 selectOne 方法,而不是其他的方法,大家或许会觉得奇怪。前面提及了 selectList、selectMap、selectCursor 等方法,这里却分析一个未提及的方法。这样做并没什么特别之处,主要原因是 selectOne 在内部会调用 selectList 方法。这里分析 selectOne 方法是为了告知大家,selectOne 和 selectList 方法是有联系的,同时分析 selectOne 方法等同于分析 selectList 方法。


    /**
     * selectOne 方法在内部调用 selectList 了方法,并取 selectList 返回值的第1个元素作为自己的返回值。如果 selectList 返回的列表元素大于1
     * @author maoqichuan
     * @date 2021/12/28 16:03
     */
    @Override
    public  T selectOne(String statement, Object parameter) {
        // 调用 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;
        }
    }

        如上,selectOne 方法在内部调用 selectList 了方法,并取 selectList 返回值的第1个元素作为自己的返回值。如果 selectList 返回的列表元素大于1,则抛出异常。上面代码比较易懂,就不多说了。下面我们来看看 selectList 方法的实现。

    @Override
    public  List selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }

    @Override
    public  List selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
            // 根据Mapper的Id,获取对应的MappedStatement对象
            MappedStatement ms = configuration.getMappedStatement(statement);
            // 以MappedStatement对象作为参数,调用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();
        }
    }

        如上,这里要来说说 executor 变量,该变量类型为 Executor。Executor 是一个接口,它的实现类如下:Mybatis源码分析(6)之SQL执行流程_第2张图片

         在上面我们也知道了默认情况下,executor 的类型为 CachingExecutor,该类是一个装饰器类,用于给目标 Executor 增加二级缓存功能。那目标 Executor 是谁呢?默认情况下是 SimpleExecutor。

        现在大家搞清楚 executor 变量的身份了,接下来继续分析 selectOne 方法的调用栈。先来看看 CachingExecutor 的 query 方法是怎样实现的。如下:

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

        上面的方法和 SimpleExecutor 父类 BaseExecutor 中的实现没什么区别,有区别的地方在于这个方法所调用的重载方法。我们继续往下看。

  @Override
  public  List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    // 获取MappedStatement对象中维护的二级缓存对象
    Cache cache = ms.getCache();
    // 若映射文件中未配置缓存或参照缓存,此时 cache = null
    if (cache != null) {
      // 判断是否需要刷新二级缓存
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // 从MappedStatement对象对应的二级缓存中获取数据
        @SuppressWarnings("unchecked")
        List list = (List) tcm.getObject(cache, key);
        if (list == null) {
          // 若缓存未命中,则调用被装饰类的 query 方法
          list = delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // 將数据存放到MappedStatement对象对应的二级缓存中
          tcm.putObject(cache, key, list);
        }
        return list;
      }
    }
    // 调用被装饰类的 query 方法
    return delegate. query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

        面的代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方法。下面来看一下 BaseExecutor 的中签名相同的 query 方法是如何实现的。

    @SuppressWarnings("unchecked")
    @Override
    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 {
                // 缓存中获取不到,则调用queryFromDatabase()方法从数据库中查询
                list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
            }
        } finally {
            queryStack--;
        }
        if (queryStack == 0) {
            // 从一级缓存中延迟加载嵌套查询结果
            for (DeferredLoad deferredLoad : deferredLoads) {
                deferredLoad.load();
            }
            // issue #601
            deferredLoads.clear();
            if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
                // issue #482
                clearLocalCache();
            }
        }
        return list;
    }

        如上,上面的方法主要用于从一级缓存中查找查询结果。若缓存未命中,再向数据库进行查询。在上面的代码中,出现了一个新的类 DeferredLoad,这个类用于延迟加载。接下来,我们来看一下 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;
    }

        上面的代码仍然不是 selectOne 方法调用栈的终点,抛开缓存操作,queryFromDatabase 最终还会调用 doQuery 进行查询。下面我们继续进行跟踪。

  @Override
  public  List 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);
      // 调用prepareStatement()方法,创建Statement对象,并进行设置参数等操作
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 调用StatementHandler对象的query()方法执行查询操作
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

        上面的方法中仍然有不少的逻辑,完全看不到即将要到达终点的趋势,不过这离终点又近了一步。接下来,我们先跳过 StatementHandler 和 Statement 创建过程,这两个对象的创建过程会在后面进行说明。这里,我们以 PreparedStatementHandler 为例,看看它的 query 方法是怎样实现的。如下:

  @Override
  public  List query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 调用PreparedStatement对象的execute()方法,执行SQL语句
    ps.execute();
    // 调用ResultSetHandler的handleResultSets()方法处理结果集
    return resultSetHandler. handleResultSets(ps);
  }

        到这里mybaits的sql执行流程差不多分析完了,整个流程还有很多细节以及逻辑,大家有兴趣的可以继续深入去学习。 

你可能感兴趣的:(Mybatis源码解析,java,mybatis源码分析,sql执行流程分析,mybatis一二级缓存)