当前位置:Gxlcms > 数据库问题 > MyBatis 源码解析:SQL 语句的执行机制

MyBatis 源码解析:SQL 语句的执行机制

时间:2021-07-01 10:21:17 帮助过:21人阅读

SqlSessionFactory 是一个工厂类,用于创建 SqlSession 对象。按照官方文档的说明,SqlSessionFactory 对象一旦被创建就应该在应用的运行期间一直存在,不应该在运行期间对其进行清除或重建。调用该工厂的 SqlSessionFactory#openSession 方法可以开启一次会话,即创建一个 SqlSession 对象。SqlSession 封装了面向数据库执行 SQL 的所有 API,它不是线程安全的,因此不能被共享,所以该对象的最佳作用域是请求或方法作用域。在上面的示例中,我们用 SqlSession 拿到相应的 Mapper 接口对象(更准确的说是一个动态代理对象),然后执行指定的数据库操作,最后关闭此次会话。

技术图片

上面这张时序图我们在本系列开篇的文章中已经引用过,描绘了 MyBatis 在一次会话生命周期内执行数据库操作的交互时序。下面对这幅图中所描绘的执行过程中类之间的交互时序关系作进一步说明,稍后我们会对图中涉及到的类和接口从源码层面进行分析,执行时序如下:

  1. 调用 SqlSessionFactory#openSession 方法创建 SqlSession 对象,开启一次会话;
  2. 调用 SqlSession#getMapper 方法获取指定的 Mapper 接口对象,这里实际上将请求委托给 Configuration#getMapper 方法执行,由前面分析映射文件解析过程时我们知道所有的 Mapper 接口都会注册到全局唯一的配置对象 Configuration 的 MapperRegistry 类型属性中;
  3. MapperRegistry 在执行 MapperRegistry#getMapper 操作时会反射创建 Mapper 接口的动态代理对象并返回;
  4. 执行对应的数据库操作方法(例如 UserMapper#selectByName),即调用 Mapper 接口动态代理对象的 MapperProxy#invoke 方法,在该方法中会获取封装执行方法的 MapperMethod 对象;
  5. 执行 MapperMethod#execute 方法,该方法会判定当前数据库操作类型(例如 SELECT),依据类型选择执行 SqlSession 对应的数据库操作方法;
  6. SqlSession 会将数据库操作委托给具体的 Executor 执行。对于动态 SQL 语句而言,在这里会依据参数执行解析;对于查询语句而言,Executor 在条件允许的情况下会尝试先从缓存中进行查询,缓存不命中才会操作具体的数据库并更新缓存。MyBatis 强大的结果集映射操作也在这里完成;
  7. 返回查询结果;
  8. 调用当前会话的 SqlSession#close 方法关闭本次会话。

整个过程围绕一次查询操作展开,虽然不能覆盖 MyBatis 执行 SQL 语句的各个方面,但主线上还是能够说明白 MyBatis 针对一次 SQL 执行的大概过程。在下面的篇幅中,我们将一起分析这一整套时序背后的实现机制。

SQL 会话管理

SqlSession 接口是 MyBatis 对外提供的数据库操作 API,是 MyBatis 的核心接口之一,用于管理一次数据库会话。围绕 SqlSession 接口的类继承关系如下图所示,其中 DefaultSqlSession 是默认的 SqlSession 实现。SqlSessionFactory 是一个工厂接口,其功能是用来创建 SqlSession 对象,该接口中声明了多个重载版本的 SqlSessionFactory#openSession 方法,DefaultSqlSessionFactory 是该接口的默认实现。上述示例程序中 SqlSessionFactoryBuilder#build 方法就是基于该实现类创建的 SqlSessionFactory 对象。SqlSessionManager 类实现了这两个接口,所以具备创建、使用,以及管理 SqlSession 对象的能力,后面会详细说明。

技术图片

SqlSession 接口中声明的方法都比较直观,感兴趣的读者可以自行阅读源码。我们来看一下针对该接口的默认实现类 DefaultSqlSession,该类的属性定义如下:

/** 全局唯一的配置对象 */
private final Configuration configuration;
/** SQL 语句执行器 */
private final Executor executor;
/** 是否自动提交事务 */
private final boolean autoCommit;
/** 标记当前缓存中是否存在脏数据 */
private boolean dirty;
/** 记录已经打开的游标 */
private List<Cursor<?>> cursorList;

DefaultSqlSession 中的方法实现基本上都是对 Executor 接口方法的封装,实现上都比较简单。这里解释一下 DefaultSqlSession#cursorList 这个属性,在 DefaultSqlSession#selectCursor 方法中会记录查询返回的游标(Cursor)对象,并在关闭 SqlSession 会话时遍历集合逐一关闭,从而防止打开的游标没有被关闭的现象。

DefaultSqlSessionFactory 是 SqlSessionFactory 接口的默认实现,用于创建 SqlSession 对象。该实现类提供了两种创建 SqlSession 对象的方式,分别是基于当前数据源创建会话和基于当前数据库连接创建会话,对应的实现如下。

  • 基于数据源创建会话
private SqlSession openSessionFromDataSource(
    ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 获取当前激活的数据库环境配置
        final Environment environment = configuration.getEnvironment();
        // 获取当前数据库环境对应的 TransactionFactory 对象,不存在的话就创建一个
        final TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 依据指定的 Executor 类型创建对应的 Executor 对象
        final Executor executor = configuration.newExecutor(tx, execType);
        // 创建 SqlSession 对象
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        this.closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}
  • 基于数据库连接创建会话
private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) {
    try {
        boolean autoCommit;
        try {
            autoCommit = connection.getAutoCommit();
        } catch (SQLException e) {
            // 考虑到很多驱动或者数据库不支持事务,设置自动提交事务
            autoCommit = true;
        }
        // 获取当前激活的数据库环境配置
        final Environment environment = configuration.getEnvironment();
        // 获取当前数据库环境对应的 TransactionFactory 对象,不存在的话就创建一个
        final TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
        final Transaction tx = transactionFactory.newTransaction(connection);
        // 依据指定的 Executor 类型创建对应的 Executor 对象
        final Executor executor = configuration.newExecutor(tx, execType);
        // 创建 SqlSession 对象
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

两种创建会话的方式在执行流程上基本一致,具体细节如上述代码注释。

SqlSessionManager 同时实现了 SqlSessionFactory 和 SqlSession 两个接口,所以具备这两个接口全部的功能。该实现类的属性定义如下:

/** 封装的 {@link SqlSessionFactory} 对象 */
private final SqlSessionFactory sqlSessionFactory;
/** 线程私有的 SqlSession 对象的动态代理对象 */
private final SqlSession sqlSessionProxy;
/** 线程私有的 SqlSession 对象 */
private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<SqlSession>();

针对 SqlSessionFactory 接口中声明的方法,SqlSessionManager 均委托给持有的 SqlSessionFactory 对象完成。对于 SqlSession 接口中声明的方法,SqlSessionManager 提供了两种实现方式:如果当前线程已经绑定了一个 SqlSession 对象,那么只要未主动调用 SqlSessionManager#close 方法,就会一直复用该线程私有的 SqlSession 对象;否则会在每次执行数据库操作时创建一个新的 SqlSession 对象,并在使用完毕之后关闭会话。相关逻辑位于 SqlSessionInterceptor 类中,这是一个定义在 SqlSessionManager 中的内部类,属性 SqlSessionManager#sqlSessionProxy 是基于该类实现的动态代理对象:

this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance(
            SqlSessionFactory.class.getClassLoader(), new Class[]{SqlSession.class}, new SqlSessionInterceptor());

SqlSessionInterceptor 类实现自 InvocationHandler 接口,对应的 SqlSessionInterceptor#invoke 方法实现如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // 获取当前线程私有的 SqlSession 对象
    final SqlSession sqlSession = localSqlSession.get();
    // 会话未被关闭
    if (sqlSession != null) {
        try {
            // 直接反射调用相应的方法
            return method.invoke(sqlSession, args);
        } catch (Throwable t) {
            throw ExceptionUtil.unwrapThrowable(t);
        }
    }
    // 没有 SqlSession,或已关闭,创建一个,使用完毕之后即关闭会话
    else {
        try (SqlSession autoSqlSession = SqlSessionManager.this.openSession()) {
            try {
                // 反射调用相应的方法
                final Object result = method.invoke(autoSqlSession, args);
                // 提交事务
                autoSqlSession.commit();
                return result;
            } catch (Throwable t) {
                // 回滚事务
                autoSqlSession.rollback();
                throw ExceptionUtil.unwrapThrowable(t);
            }
        }
    }
}

SqlSessionInterceptor 首先会尝试获取线程私有的 SqlSession 对象,对于未绑定的线程来说会创建一个新的 SqlSession 对象,并在使用完毕之后立刻关闭。

动态代理 Mapper 接口

MyBatis 要求所有的 Mapper 都定义成接口的形式,这主要是为了配合 JDK 内置的动态代理机制,JDK 内置的动态代理要求被代理的类必须抽象出一个接口。常用的动态代理除了 JDK 内置的方式,还有基于 CGlib 等第三方组件的方式,MyBatis 采用了 JDK 内置的方式创建 Mapper 接口的动态代理对象。

我们先来复习一下 JDK 内置的动态代理机制,假设现在有一个接口 Mapper 及其实现类如下:

public interface Mapper {
    int select();
}

public class MapperImpl implements Mapper {
    @Override
    public int select() {
        System.out.println("do select.");
        return 0;
    }
}

现在我们希望在方法执行之前打印一行调用日志,基于动态代理的实现方式如下。我们需要定义一个实现了 InvocationHandler 接口的代理类,然后在其 InvocationHandler#invoke 方法中实现增强逻辑:

public class MapperProxy implements InvocationHandler {

    private Mapper mapper;

    public MapperProxy(Mapper mapper) {
        this.mapper = mapper;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("before invoke.");
        return method.invoke(this.mapper, args);
    }

}

客户端调用代码:

Mapper mapper = new MapperImpl();
Mapper mapperProxy = (Mapper) Proxy.newProxyInstance(
        mapper.getClass().getClassLoader(), mapper.getClass().getInterfaces(), new MapperProxy(mapper));
mapperProxy.select();

回到 MyBatis 框架本身,我们在执行目标数据库操作时,一般会直接调用目标 Mapper 接口的相应方法,这里框架返回给我们的实际上是 Mapper 接口的动态代理类对象。MyBatis 基于 JDK 的动态代理机制实现了 Mapper 接口中声明的方法,这其中包含了 获取 SQL 语句、参数绑定、缓存操作、数据库操作,以及结果集映射处理 等步骤,下面就 Mapper 接口动态代理机制涉及到的相关类和方法进行分析。

上一篇在分析映射文件时我们介绍了在 MapperRegistry#knownMappers 属性中记录了 Mapper 接口与 MapperProxyFactory 的映射关系,MapperProxyFactory 顾名思义是 MapperProxy 的工厂类,其中定义了创建 Mapper 接口代理对象的方法,如下:

public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return this.newInstance(mapperProxy);
}

protected T newInstance(MapperProxy<T> mapperProxy) {
    // 创建 Mapper 接口对应的动态代理对象(基于 JDK 内置的动态代理机制)
    return (T) Proxy.newProxyInstance(
        mapperInterface.getClassLoader(),
        new Class[] {mapperInterface},
        mapperProxy);
}

来看一下 MapperProxy 实现,该类实现了 InvocationHandler 接口,对应的 InvocationHandler#invoke 实现如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        // 对于 Object 类中声明的方法,直接调用
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 对于非 Object 类中声明的方法
        else {
            // 获取方法关联的 MapperMethodInvoker 对象,并执行数据库操作
            return this.cachedInvoker(method).invoke(proxy, method, args, sqlSession);
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
}

上述方法的核心逻辑在于获取当前执行方法 Method 对象对应的 MapperMethodInvoker 方法调用器,并执行 MapperMethodInvoker#invoke 方法触发对应的数据库操作。

围绕 MapperMethodInvoker 接口,MyBatis 提供了两种实现,即 DefaultMethodInvoker 和 PlainMethodInvoker,其中前者用于支持 JDK 7 引入的动态类型语言特性,后者则是对 MapperMethod 的封装。MapperMethod 中主要定义两个内部类:

  • SqlCommand:用于封装方法关联的 SQL 语句名称和类型。
  • MethodSignature:用来封装方法相关的签名信息。

先来看一下 SqlCommand 的具体实现,该类定义了 SqlCommand#name 和 SqlCommand#type 两个属性,分别用于记录对应 SQL 语句的名称和类型,并在构造方法中实现了相应的解析逻辑和初始化操作,如下:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
    // 获取方法名称
    final String methodName = method.getName();
    // 获取方法隶属的类或接口的 Class 对象
    final Class<?> declaringClass = method.getDeclaringClass();
    // 解析方法关联的 SQL 语句对应的 MappedStatement 对象(用于封装 SQL 语句)
    MappedStatement ms = this.resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
    // 未找当前方法对应的 MappedStatement 对象
    if (ms == null) {
        // 如果对应方法注解了 @Flush,表示执行缓存的批量更新语句,则进行标记
        if (method.getAnnotation(Flush.class) != null) {
            name = null;
            type = SqlCommandType.FLUSH;
        } else {
            throw new BindingException(
                "Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
        }
    }
    // 找当前方法对应的 MappedStatement 对象,初始化 SQL 语句名称和类型
    else {
        name = ms.getId();
        type = ms.getSqlCommandType();
        if (type == SqlCommandType.UNKNOWN) {
            throw new BindingException("Unknown execution method for: " + name);
        }
    }
}

private MappedStatement resolveMappedStatement(
    Class<?> mapperInterface, String methodName, Class<?> declaringClass, Configuration configuration) {
    // 接口名称.方法名
    String statementId = mapperInterface.getName() + "." + methodName;

    // 方法存在关联的 SQL 语句,则获取封装该 SQL 语句的 MappedStatement 对象
    if (configuration.hasStatement(statementId)) {
        return configuration.getMappedStatement(statementId);
    }
    // 已经递归到该方法隶属的最上层类,但是仍然没有找到关联的 MappedStatement 对象
    else if (mapperInterface.equals(declaringClass)) {
        return null;
    }

    // 沿着继承关系向上递归检索
    for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
            // 递归检索
            MappedStatement ms = this.resolveMappedStatement(
                superInterface, methodName, declaringClass, configuration);
            if (ms != null) {
                return ms;
            }
        }
    }
    return null;
}

SqlCommand 在实例化时所做的主要工作就是解析当前 Mapper 方法关联的 SQL 对应的 MappedStatement 对象,并初始化记录的 SQL 语句名称和类型,整个解析过程如上述代码注释。

再来看一下 MethodSignature 类,该类用于封装一个具体 Mapper 方法的相关签名信息,其中定义的方法实现都比较简单,这里列举一下其属性定义:

/** 标记返回值是否是 {@link java.util.Collection} 或数组类型 */
private final boolean returnsMany;
/** 标记返回值是否是 {@link Map} 类型 */
private final boolean returnsMap;
/** 标记返回值是否是 {@link Void} 类型 */
private final boolean returnsVoid;
/** 标记返回值是否是 {@link Cursor} 类型 */
private final boolean returnsCursor;
/** 标记返回值是否是 {@link Optional} 类型 */
private final boolean returnsOptional;
/** 返回值类型 */
private final Class<?> returnType;
/** 对于 Map 类型的返回值,用于记录 key 的别名 */
private final String mapKey;
/** 标记参数列表中 {@link ResultHandler} 的下标 */
private final Integer resultHandlerIndex;
/** 标记参数列表中 {@link RowBounds} 的下标 */
private final Integer rowBoundsIndex;
/** 参数名称解析器 */
private final ParamNameResolver paramNameResolver;

上面属性中重点介绍一下 ParamNameResolver 这个类,它的作用在于解析 Mapper 方法的参数列表,以便于在方法实参和方法关联的 SQL 语句的参数之间建立映射关系。其中一个比较重要的属性是 ParamNameResolver#names,定义如下:

/**
 * 记录参数在参数列表中的下标和参数名称之间的对应关系。
 * 参数名称通过 {@link Param} 注解指定,如果没有指定则使用参数下标作为参数名称,
 * 需要注意的是,如果参数列表中包含 {@link RowBounds} 或 {@link ResultHandler} 类型的参数,
 * 这两种功能型参数不会记录到集合中,此时如果用下标表示参数名称,索引值 key 与对应的参数名称(实际索引)可能会不一致。
 *
 * <p>
 * The key is the index and the value is the name of the parameter.<br />
 * The name is obtained from {@link Param} if specified. When {@link Param} is not specified,
 * the parameter index is used. Note that this index could be different from the actual index
 * when the method has special parameters (i.e. {@link RowBounds} or {@link ResultHandler}).
 * </p>
 * <ul>
 * <li>aMethod(@Param("M") int a, @Param("N") int b) -&gt; {{0, "M"}, {1, "N"}}</li>
 * <li>aMethod(int a, int b) -&gt; {{0, "0"}, {1, "1"}}</li>
 * <li>aMethod(int a, RowBounds rb, int b) -&gt; {{0, "0"}, {2, "1"}}</li>
 * </ul>
 */
private final SortedMap<Integer, String> names;

我把它的英文注释和我的理解都写在代码注释中,应该可以清楚理解该属性的作用。至于为什么需要跳过 RowBounds 和 ResultHandler 这两个类型的参数,是因为前者用于设置 LIMIT 参数,后者用于设置结果集处理器,所以都不是真正意义上的参数,按照我的话说这两种类型的参数都是功能型的参数。

ParamNameResolver 在构造方法中实现了对参数列表的解析,如下:

public ParamNameResolver(Configuration config, Method method) {
    // 获取参数类型列表
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 获取参数列表上的注解列表
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;

    // 遍历处理方法所有的参数
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
        // 跳过 RowBounds 和 ResultHandler 类型参数
        if (isSpecialParameter(paramTypes[paramIndex])) {
            continue;
        }

        // 查找当前参数是否有 @Param 注解
        String name = null;
        for (Annotation annotation : paramAnnotations[paramIndex]) {
            // 获取注解指定的参数名称
            if (annotation instanceof Param) {
                hasParamAnnotation = true;
                name = ((Param) annotation).value();
                break;
            }
        }

        // 没有 @Param 注解
        if (name == null) {
            // 基于配置开关决定是否获取参数的真实名称
            if (config.isUseActualParamName()) {
                name = this.getActualParamName(method, paramIndex);
            }
            // 使用下标作为参数名称
            if (name == null) {
                // use the parameter index as the name ("0", "1", ...)
                // gcode issue #71
                name = String.valueOf(map.size());
            }
        }
        map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
}

整个过程概括来说就是遍历处理指定方法的参数列表,忽略 RowBounds 和 ResultHandler 类型的参数,并判断参数前面是否有 @Param 注解,如果有则尝试以注解指定的字符串作为参数名称,否则基于配置决定是否采用参数的真实名称作为这里的参数名,再不济就采用下标值作为参数名称。

考虑到会忽略 RowBounds 和 ResultHandler 两种类型的参数,但是属性 ParamNameResolver#names 对应的 key 又是递增的,所以就可能出现在以下标值作为参数名称时,参数名称与对应下标值不一致的情况。例如,假设有一个方法的参数列表为 (int a, RowBounds rb, int b),因为有 RowBounds 类型夹在中间,如果以下标值作为参数名称的最终解析结果就是 {0, "0"}, {2, "1"},下标与具体的参数名称不一致。

ParamNameResolver 中还有一个比较重要的方法 ParamNameResolver#getNamedParams,用于关联实参和形参列表,其中 args 参数是用户传递的实参数组,方法基于前面的参数列表解析结果将传递的实现与对应的方法参数进行关联,最终记录到 Object 对象中进行返回,实现如下:

public Object getNamedParams(Object[] args) {
    // names 属性记录参数在参数列表中的下标和参数名称之间的对应关系
    final int paramCount = names.size();
    // 无参方法,直接返回
    if (args == null || paramCount == 0) {
        return null;
    }
    // 没有 @Param 注解,且只有一个参数
    else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()];
    }
    // 有 @Param 注解,或存在多个参数
    else {
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        // 遍历处理参数列表中的非功能性参数
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            // 记录参数名称与参数值之间的映射关系
            param.put(entry.getValue(), args[entry.getKey()]);
            // 构造一般参数名称,即 (param1, param2, ...) 形式参数
            final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
            // 以“param + 索引”的形式再记录一次,如果 @Param 指定的参数名称已经是这种形式则不覆盖
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

做了这么多的铺垫,是时候回来继续分析 MapperMethod 的核心方法 MapperMethod#execute 了。该方法的作用在于委托 SqlSession 对象执行方法对应的 SQL 语句,实现如下:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case INSERT: {
            // 关联实参与方法参数列表
            Object param = method.convertArgsToSqlCommandParam(args);
            // 调用 SqlSession#insert 方法执行插入操作,并对执行结果进行转换
            result = this.rowCountResult(sqlSession.insert(command.getName(), param));
            break;
        }
        case UPDATE: {
            // 关联实参与方法参数列表
            Object param = method.convertArgsToSqlCommandParam(args);
            // 调用 SqlSession#update 方法执行更新操作,并对执行结果进行转换
            result = this.rowCountResult(sqlSession.update(command.getName(), param));
            break;
        }
        case DELETE: {
            // 关联实参与方法参数列表
            Object param = method.convertArgsToSqlCommandParam(args);
            // 调用 SqlSession#delete 方法执行删除操作,并对执行结果进行转换
            result = this.rowCountResult(sqlSession.delete(command.getName(), param));
            break;
        }
        case SELECT:
            // 返回值是 void 类型,且指定了 ResultHandler 处理结果集
            if (method.returnsVoid() && method.hasResultHandler()) {
                this.executeWithResultHandler(sqlSession, args);
                result = null;
            }
            // 返回值为 Collection 或数组
            else if (method.returnsMany()) {
                result = this.executeForMany(sqlSession, args);
            }
            // 返回值为 Map 类型
            else if (method.returnsMap()) {
                result = this.executeForMap(sqlSession, args);
            }
            // 返回值为 Cursor 类型
            else if (method.returnsCursor()) {
                result = this.executeForCursor(sqlSession, args);
            }
            // 处理其它返回类型
            else {
                Object param = method.convertArgsToSqlCommandParam(args);
                result = sqlSession.selectOne(command.getName(), param);
                if (method.returnsOptional() // Optional 类型
                    && (result == null || !method.getReturnType().equals(result.getClass()))) {
                    result = Optional.ofNullable(result);
                }
            }
            break;
        case FLUSH:
            // 如果方法注解了 @Flush,则执行 SqlSession#flushStatements 方法提交缓存的批量更新操作
            result = sqlSession.flushStatements();
            break;
        default:
            throw new BindingException("Unknown execution method for: " + command.getName());
    }
    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;
}

上述方法会依据具体的 SQL 语句类型分而治之。对于 INSERT、UPDATE,以及 DELETE 类型而言,会先调用 MethodSignature#convertArgsToSqlCommandParam 方法关联实参与方法形参,本质上是调用前面介绍的 ParamNameResolver#getNamedParams 方法。然后就是调用 SqlSession 对应的方法执行数据库操作,并通过方法 MapperMethod#rowCountResult 对结果进行类型转换。关于 SqlSession 相关方法的具体实现留到下一节针对性介绍。对于 SELECT 类型而言,则需要考虑不同的返回类型,分为 void、Collection、数组、Map、Cursor,以及对象几类情况,这里所做的都是对于参数或返回结果的处理,核心逻辑也都位于 SqlSession 中,在这一层面的实现都比较简单,就不再一一展开。对于 FLUSH 类型来说,官方文档的说明如下:

如果使用了这个注解,它将调用定义在 Mapper 接口中的 SqlSession#flushStatements 方法。

具体的实现也就位于这里。

SQL 语句执行器

Executor 接口声明了基本的数据库操作,前面在介绍 SqlSession 时曾描述 SqlSession 是 MyBatis 框架对外提供的 API 接口,其中声明了对数据库的基本操作方法,而这些操作方法基本上都是对 Executor 方法的封装。Executor 接口定义如下:

public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    /** 执行数据库更新操作:update、insert、delete */
    int update(MappedStatement ms, Object parameter) throws SQLException;
    /** 执行数据库查询操作 */
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
    /** 执行数据库查询操作,返回游标对象 */
    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
    /** 批量提交 SQL 语句 */
    List<BatchResult> flushStatements() throws SQLException;
    /** 提交事务 */
    void commit(boolean required) throws SQLException;
    /** 回滚事务 */
    void rollback(boolean required) throws SQLException;
    /** 创建缓存 key 对象 */
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
    /** 判断是否缓存 */
    boolean isCached(MappedStatement ms, CacheKey key);
    /** 清空一级缓存 */
    void clearLocalCache();
    /** 延迟加载一级缓存中的数据 */
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
    /** 获取事务对象 */
    Transaction getTransaction();
    /** 关闭当前 Executor */
    void close(boolean forceRollback);
    /** 是否已经关闭 */
    boolean isClosed();
    /** 设置装饰的 Executor 对象 */
    void setExecutorWrapper(Executor executor);
}

围绕 Executor 接口的类继承关系如下图,其中 CachingExecutor 实现类用于为 Executor 提供二级缓存支持。

技术图片

BaseExecutor 抽象类实现了 Executor 接口中声明的所有方法,并抽象了 4 个模板方法交由子类实现,这 4 个方法分别是:doUpdate、doFlushStatements、doQuery,以及 doQueryCursor。SimpleExecutor 派生自 BaseExecutor 抽象类,并为这 4 个模板方法提供了最简单的实现。ReuseExecutor 如其名,提供了重用的特性,提供对 Statement 对象的重用,以减少 SQL 预编译,以及创建和销毁 Statement 对象的开销。BatchExecutor 实现类则提供了对 SQL 语句批量执行的特性,也是针对提升性能的一种优化实现。

缓存结构设计

考虑到 Executor 在执行数据库操作时与缓存操作存在密切联系,所以在具体介绍 Executor 的实现之前我们先来了解一下 MyBatis 的缓存机制。

在谈论数据库架构设计时往往需要引入缓存的概念,数据库是相对脆弱且耗时的,所以需要尽量避免请求落库。在实际项目架构设计中,我们一般会引入 Redis、Memcached 这一类的组件对数据进行缓存,MyBatis 作为一个强大的 ORM 框架,也为缓存提供了内建的实现。前面我们在分析配置文件加载与解析时曾介绍过 MyBatis 缓存组件的具体实现,MyBatis 在数据存储上采用 HashMap 作为基本存储结构,并提供了多种装饰器从多个方面为缓存增加相应的特性。

本小节我们关注的是 MyBatis 在缓存结构方面的设计,MyBatis 缓存从结构上可以分为 一级缓存 和 二级缓存,一级缓存相对于二级缓存在粒度上更细,生命周期也更短。

技术图片

上图描绘了 MyBatis 缓存的结构设计,当我们发起一次数据库查询时,如果启用了二级缓存的话,MyBatis 首先会从二级缓存中检索查询结果,如果缓存不命中则会继续检索一级缓存,只有在这两层缓存都不命中的情况下才会查询数据库,最后会以数据库返回的结果更新一级缓存和二级缓存。

MyBatis 的 一级缓存是会话级别的缓存(生命周期与本次会话相同),当我们开启一次数据库会话时,框架默认会为本次会话绑定一个一级缓存对象。此类缓存主要应对在一个会话范围内的冗余查询操作,比如使用同一个 SqlSession 对象同时连续执行多次相同的查询语句。这种情况下每次查询都落库是没有必要的,因为短时间内数据库变化的可能性会很小,但是每次都落库却是一笔不必要的开销。一级缓存默认是开启的,且无需进行配置,即一级缓存对开发者是透明的,如果确实希望干预一级缓存的内在运行,可以借助于插件来实现。

对于二级缓存而言,默认也是开启的,MyBatis 提供了相应的治理选项,具体可以参考官方文档。二级缓存是应用级别的缓存,随着服务的启动而存在,并随着服务的关闭消亡。前面我们在分析 <cache/> 和 <cache-ref/> 标签时介绍了一个二级缓存会与一个具体的 namespace 绑定,并且支持引用一个已定义 namespace 缓存,即多个 namespace 可以共享同一个缓存。

本小节从整体结构上对 MyBatis 的缓存实现机制进行说明,目的在于对 MyBatis 的缓存有一个整体感知,关于一级缓存和二级缓存的具体实现,留到下面介绍分析 Executor 接口具体实现时穿插说明。

Statement 处理器

StatementHandler 接口及其实现类是 Executor 实现的基础,可以将其看作是 MyBatis 与数据库操作之间的纽带,实现了对 java.sql.Statement 对象的获取,以及 SQL 语句参数绑定与执行的逻辑。StatementHandler 接口及其实现类的类继承关系如下图所示:

技术图片

其中 BaseStatementHandler 中实现了一些公共的逻辑;SimpleStatementHandler、PreparedStatementHandler,以及 CallableStatementHandler 实现类分别对应 Statement、PreparedStatement 和 CallableStatement 的相关实现;RoutingStatementHandler 并没有添加新的实现,而是对前面三种 StatementHandler 实现类的封装,它会在构造方法中依据当前传递的 Statement 类型创建对应的 StatementHandler 实现类对象。

StatementHandler 接口定义如下:

public interface StatementHandler {

    /** 获取对应的 {@link Statement } 对象 */
    Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;
    /** 绑定 Statement 执行 SQL 时需要的实参 */
    void parameterize(Statement statement) throws SQLException;
    /** 批量执行 SQL 语句 */
    void batch(Statement statement) throws SQLException;
    /** 执行数据库更新操作:insert、update、delete */
    int update(Statement statement) throws SQLException;
    /** 执行 select 操作 */
    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;
    /** 执行 select 操作,返回游标对象 */
    <E> Cursor<E> queryCursor(Statement statement) throws SQLException;
    /** 获取对应的 SQL 对象 */
    BoundSql getBoundSql();
    /** 获取对应的 {@link ParameterHandler} 对象,用于参数绑定 */
    ParameterHandler getParameterHandler();
}

首先来看一下 BaseStatementHandler 实现,该类中主要实现了获取 Statement 对象的逻辑,该类的属性定义如下:

protected final Configuration configuration;
protected final ObjectFactory objectFactory;
protected final TypeHandlerRegistry typeHandlerRegistry;
/** 处理结果集映射 */
protected final ResultSetHandler resultSetHandler;
/** 用于为 SQL 语句绑定实参 */
protected final ParameterHandler parameterHandler;
/** SQL 语句执行器 */
protected final Executor executor;
/** 对应 SQL 语句标签对象 */
protected final MappedStatement mappedStatement;
/** 封装 LIMIT 参数 */
protected final RowBounds rowBounds;
/** 可执行的 SQL 语句 */
protected BoundSql boundSql;

BaseStatementHandler 之于 StatementHandler#prepare 方法的实现如下:

public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
        // 从数据库连接中获取 Statement 对象,由子类实现
        statement = this.instantiateStatement(connection);
        // 设置超时时间
        this.setStatementTimeout(statement, transactionTimeout);
        // 设置返回的行数
        this.setFetchSize(statement);
        return statement;
    } catch (SQLException e) {
        this.closeStatement(statement);
        throw e;
    } catch (Exception e) {
        this.closeStatement(statement);
        throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
}

上述方法首先会调用 BaseStatementHandler#instantiateStatement 方法获取一个 Statement 对象,这是一个模板方法交由子类实现;然后对拿到的 Statement 对象设置超时时间和返回的行数属性。

BaseStatementHandler 中定义了 ParameterHandler 类型的属性,主要用于为包含 ? 占位符的 SQL 语句绑定实参。ParameterHandler 接口定义如下:

public interface ParameterHandler {

    /** 获取输出类型参数 */
    Object getParameterObject();

    /** 为 SQL 语句绑定实参 */
    void setParameters(PreparedStatement ps) throws SQLException;

}

其中,方法 ParameterHandler#getParameterObject 与存储过程相关,下面主要分析一下 ParameterHandler#setParameters 方法的实现。该方法用来为 SQL 语句绑定实参,具体操作等同于我们在直接使用 PreparedStatement 对象时注入相应类型的参数填充 SQL 语句。DefaultParameterHandler 是目前该接口的唯一实现,其 DefaultParameterHandler#setParameters 方法实现如下:

public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 获取 BoundSql 中记录的参数映射关系列表
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        // 遍历为 SQL 语句绑定对应的参数值
        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 params
                    value = boundSql.getAdditionalParameter(propertyName);
                }
                // 用户未传递实参
                else if (parameterObject == null) {
                    value = null;
                }
                // 实参类型存在对应的类型处理器,即已经是最终的参数值
                else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                }
                // 获取实参对象中对                    

人气教程排行