MyBatis 源码分析之 Select 语句执行(上)

news2024/11/16 9:40:21
  • 三哥

内容来自【自学星球】

欢迎大家来了解我的星球,和星主(也就是我)一起学习 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> T selectOne(String statement, Object parameter) {
    // Popular vote was to return null on 0 results and throw exception on too many.
    // 调用 selectList 获取结果
    List<T> list = this.<T>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 <E> List<E> 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 <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
        // 通过MappedStatement的Id获取 MappedStatement
        MappedStatement 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 <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 获取 BoundSql
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 创建 CacheKey
    CacheKey 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<ParameterMapping> parameterMappings;
    // 运行时参数,即用户传入的参数,比如 Article 对象,或是其他的参数
    private final Object parameterObject;
    // 附加参数集合,用于存储一些额外的信息,比如 datebaseId 等
    private final Map<String, Object> 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<ParameterMapping> 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) {
    // 创建 DynamicContext
    DynamicContext context = new DynamicContext(configuration, parameterObject);

    // 解析 SQL 片段,并将解析结果存储到 DynamicContext 中,
    // 这里会将${}替换成method对应的运行时参数,也会解析<if><where>等SqlNode
    rootSqlNode.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 获取 BoundSql
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);

    // 将 DynamicContext 的 ContextMap 中的内容拷贝到 BoundSql 中
    for (Map.Entry<String, Object> 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 以及 databaseId
        bindings.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();
    }

    //继承HashMap
    static class ContextMap extends HashMap<String, Object> {

        private MetaObject parameterMetaObject;

        public ContextMap(MetaObject parameterMetaObject) {
            this.parameterMetaObject = parameterMetaObject;
        }

        @Override
        public 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<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 遍历 SqlNode 集合
        for (SqlNode sqlNode : contents) {
            // 调用 salNode 对象本身的 apply 方法解析 sql
            sqlNode.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;
    }

    @Override
    public 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;

    @Override
    public 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;
        }

        @Override
        public 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();
    }

    @Override
    public 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<String, Object> additionalParameters) {
    // 创建 #{} 占位符处理器
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);

    // 创建 #{} 占位符解析器
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);

    // 解析 #{} 占位符,并返回解析结果字符串
    String sql = parser.parse(originalSql);

    // 封装解析结果到 StaticSqlSource 中,并返回,因为所有的动态参数都已经解析了,可以封装成一个静态的SqlSource
    return 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());

    // 忽略代码

    // 返回解析完成的 sql
    return 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<String, String> propertiesMap = parseParameterMapping(content);
    String property = propertiesMap.get("property");
    Class<?> propertyType;

    // metaParameters 为 DynamicContext 成员变量 bindings 的元信息对象
    if (metaParameters.hasGetter(property)) { // issue #448 get type from additional params
        propertyType = 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 赋值给 javaType
    Class<?> javaType = propertyType;
    String typeHandlerAlias = null;

    // 遍历 propertiesMap
    for (Map.Entry<String, String> 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)) {
            // 解析 jdbcType
            builder.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<ParameterMapping> parameterMappings;
    private final Configuration configuration;

    public StaticSqlSource(Configuration configuration, String sql) {
        this(configuration, sql, null);
    }

    public StaticSqlSource(Configuration configuration, String sql, List<ParameterMapping> parameterMappings) {
        this.sql = sql;
        this.parameterMappings = parameterMappings;
        this.configuration = configuration;
    }

    @Override
    public 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 <E> List<E> 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);
            @SuppressWarnings("unchecked")
            List<E> list = (List<E>) tcm.getObject(cache, key);
            if (list == null) {
                // 若缓存未命中,则调用被装饰类的 query 方法,也就是SimpleExecutor的query方法
                list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                tcm.putObject(cache, key, list); // issue #578 and #116
            }
            return list;
        }
    }

    // 调用被装饰类的 query 方法,这里的delegate我们知道应该是SimpleExecutor
    return delegate.<E> 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 <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());
    if (closed) {
        throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 从一级缓存中获取缓存项,一级缓存我们也下一节单独讲
        list = resultHandler == null ? (List<E>) 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 #601
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            // issue #482
            clearLocalCache();
        }
    }
    return list;
}

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

org.apache.ibatis.executor.BaseExecutor#queryFromDatabase

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 {
        // 调用 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 <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
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
        // 创建 Statement
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 执行查询操作
        return handler.<E>query(stmt, resultHandler);
    } finally {
        // 关闭 Statement
        closeStatement(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) {
    // 创建具有路由功能的 StatementHandler
    StatementHandler 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<ParameterMapping>()).build();
            mappedStatement.resultMaps = new ArrayList<ResultMap>();
            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来获取Connection
    Connection 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;
    }

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

    /**
     * commit()功能 
     * @throws SQLException
     */
    @Override
    public 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
     */
    @Override
    public 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
     */
    @Override
    public 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);
    }

    @Override
    public 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,则直接返回这个connection
    return 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);
    //创建一个SqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    try {
        EmployeeMapper employeeMapper = sqlSession.getMapper(Employee.class);
        UserMapper userMapper = sqlSession.getMapper(User.class);
        List<Employee> allEmployee = employeeMapper.getAll();
        List<User> 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 {
        // 创建 Statement
        statement = instantiateStatement(connection);
        // 设置超时和 FetchSize
        setStatementTimeout(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 方法创建 PreparedStatement
    if (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<ParameterMapping> 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 params
                    value = 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.<E>query(stmt, resultHandler);

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

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

该方法主要有两个步骤:

  • 执行 SQL
  • 解析返回结果

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

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

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


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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/22999.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

盘点敏捷项目失败的6个主要原因

敏捷项目管理及其相关的方法和价值观正在迅速改变着许多企业的运作方式。改善灵活性、提高协作和生产力以及加强客户联系的承诺&#xff0c;敏捷的种种好处摆在企业决策者面前。 然而&#xff0c;直接运行成功的敏捷项目并不是必然的。是什么原因导致敏捷管理失败&#xff1f…

公众号免费调用题库

公众号免费调用题库 本平台优点&#xff1a; 多题库查题、独立后台、响应速度快、全网平台可查、功能最全&#xff01; 1.想要给自己的公众号获得查题接口&#xff0c;只需要两步&#xff01; 2.题库&#xff1a; 查题校园题库&#xff1a;查题校园题库后台&#xff08;点击…

Reading Note(8) ——GNN for DSE

这篇博客是一篇论文阅读札记&#xff0c;这篇论文的主题是使用GNN来优化加速器设计中的设计空间探索耗时过长的问题。 这篇文章的标题是《Enabling Automated FPGA Accelerator Optimization Using Graph Neural Networks》&#xff0c;值得注意的是这是它的预印版&#xff0c…

Java-内部类

内部类的概念 认识内部类 一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称之为内部类inner class&#xff0c;嵌套该内部类的类称为外部类。就像双重for循环&#xff0c;外部for循环里面包含着另一个内层循环。内部类是类的第5大成员&#xff0c;[属性&#xff0c;方法…

让我们进入面向对象的世界(一)

让我们进入面向对象的世界 文章目录让我们进入面向对象的世界开场白一.面向对象概述二.认识对象和类2.1什么是类2.2 什么是对象呢&#xff1f;2.3 让我们来创建第一个对象2.3 让我们进一步了解&#xff0c;我们针对对象的操作&#xff0c;是怎样的开场白 大家好&#xff01;&a…

SpringMVC请求、响应与异步请求

文章目录SpringMVC核心架构的具体流程步骤一、SpringMVC请求与响应1、SpringMVC Handle原理与应用1.1 概念1.2 Spring MVC Handler的三种写法2、SpringMVC 视图解析器2.1 概念2.2 配置视图解析器二、SpringMVC异步请求1、 Ajax基本配置2、 异步与同步优缺点&#xff1a;如何设置…

基于TMI8421的3D打印机步进电机解决方案

打印机一直是工作中不可缺少的一部分&#xff0c;当下&#xff0c;随着3D打印技术的推广&#xff0c;3D打印机逐渐进入我们的生活与工作当中。每个人都期望可以在办公室环境下安静快速的打印&#xff0c;更高效地完成每项打印工作&#xff1b;更生动逼真的重现理想的3D模型。而…

实战:如何优雅地扩展Log4j配置?

前言 Log4j 日志框架我们经常会使用到&#xff0c;最近&#xff0c;我就遇到了一个与日志配置相关的问题。简单来说&#xff0c;就是在原来日志配置的基础上&#xff0c;指定类的日志打印到指定的日志文件中。 这样讲述可能不是那么好理解&#xff0c;且听我从需求来源讲起。…

【10】leetcode note

题目&#xff1a; 799. 香槟塔 个人总结 799. 香槟 我们把玻璃杯摆成金字塔的形状&#xff0c;其中 第一层 有 1 个玻璃杯&#xff0c; 第二层 有 2 个&#xff0c;依次类推到第 100 层&#xff0c;每个玻璃杯 (250ml) 将盛有香槟。 从顶层的第一个玻璃杯开始倾倒一些香槟&…

基于铁犀牛ironrhino平台的税务档案管理系统

目录 摘要 2 引言 5 1.1 选题背景 6 1.2 国内外研究现状 7 1.3课题研究的重要性 8 2. 系统的需求与分析 8 2.1 系统的功能介绍 9 2.1.1 业务信息描述 9 2.1.2 系统功能说明 10 2.1.3 系统的开发目标 11 2.2 系统分析 12 2.2.1 铁犀牛的功能 12 2.2.2 铁犀牛工作原理 13 2.2.3 铁…

翻阅必备----Java窗口组件,容器,布局,监听,事件 API大全

---------------------------------------------------------------------------------------- &#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 秩沅 原创 收录于专栏 java ⭐Jav…

将存在课题的过程可视化,丰田的“自工程完结”是什么?

将存在课题的过程可视化&#xff0c;丰田的“自工程完结”是什么? “全日本的公司是不是都发生了奇怪的事情呢?人们常说日本人很勤劳。所以要拼命努力。但是&#xff0c;有很多人拼命努力却毫无成果。(中略)这样才不会有动力。明明很努力却没有成果&#xff0c;我认为这才是奇…

将SpringBOOT项目 打成 war 包 并 部署到 Tomcat

当前环境&#xff1a;Windows Tomcat版本&#xff1a;tomcat8.5 SpringBoot版本&#xff1a; 2.2.3 1. pom.xml 修改打包方式 <packaging>war</packaging> 2.加入SpringBoot打包插件&#xff08;pom.xml&#xff09; <build><plugins><plugin&g…

Jmeter的使用说明

一、安装Jmeter工具 链接&#xff1a;https://pan.baidu.com/s/1ZYc15eq9DO-r0ChKHxMXlg?pwdckcd 提取码&#xff1a;ckcd --来自百度网盘超级会员V5的分享二、Jmeter的常用元器件说明 jmeter八大元件件&#xff1a;取样器&#xff0c;前置处理器&#xff0c;后置处理器&a…

计算机网络——第六章笔记(1)

传输层 传输层是层是整个协议栈(TCP/IP)的核心。 任务&#xff1a;是提供可靠的、高效的数据传输。 面向连接的服务 1、端到端的连接管理 建立连接 数据传输 释放连接 2、流控制 3、差错控制 传输环境&#xff1a;通信子网、物理信道。 传输服务和网络服务的两个主…

网络了解编程五层协议

一:了解 1.了解一下网络: 局域网(LAN),一个上课的机房,多个连在同一个路由器上的设备,就是在一个局域网中---打游戏 (局域网内的主机之间能方便的进行网络通信&#xff0c;又称为内网&#xff1b;局域网和局域网之间在没有连接的情况下&#xff0c;是无法通信的) 广域网(WAN) ,…

无线 LAN 服务概述

无线 LAN 服务是 Windows Server 2008 R2 和 Windows Server 2008 中的一项功能&#xff0c;可用于启用无线 WLAN 自动配置服务&#xff0c;以及配置 WLAN 自动配置以自动启动。一旦启用后&#xff0c;WLAN 自动配置会动态选择计算机将自动连接的无线网络&#xff0c;并配置无线…

项目管理的四大模型,PM必懂的事半功倍模型!

瀑布模型、迭代模型、增量模型、原型模型&#xff0c;是项目管理常见的四种模型。每种模型都有其优缺点和适用的项目类型。项目经理针对不同的项目用对模型&#xff0c;才能起到事半功倍的作用。 今天就讲讲这四种模型及其优缺点&#xff1a; 如果你需要项目管理相关资料可拉…

代码质量与安全 | “吃狗粮”能够影响到代码质量?来了解一下!

“dogfooding”是什么&#xff1f;乍一看&#xff0c;这就是“吃狗粮”的意思&#xff0c;但其实这来源于一句俚语&#xff1a;“Eat your own dog food”&#xff0c;直译过来就是“吃自己的狗粮”&#xff0c;常用于描述公司使用自己产品的这一种情况。 “吃自己的狗粮”实践…

(更新中)【后端入门到入土!】Java+Servlet+JDBC+SSM+SpringBoot+SpringCloud 基础入门

目录 第一部分&#xff1a;Java 基础语法&#xff08;已完结&#xff09; 第二部分&#xff1a;Java 高级&#xff08;已完结&#xff09; 第三部分&#xff1a;Servlet&#xff08;待更新……&#xff09; 第四部分&#xff1a;JDBC&#xff08;待更新……&#xff09; 第…