转载

MyBatis 的秘密(三)StatementHandler

StatementHandler

前面我们说过, Executor 的主要职责是执行底层映射语句。

但是通过源码我们可以发现, Executor 执行的这些功能,都是通过 StatementHandler 来完成的, Executor 只是负责缓存或者选择调用 StatmentHandler 的具体的方法。

下面来看看 StatementHandler 的具体实现

//新建StatmentHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    //创建RoutingStatementHandler 
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    //添加插件
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
}

这个方法是所有创建 StatementHandler 的方法,可以看到都是统一的创建 RoutingStatementHandler .

RoutingStatementHandler 是一个代理类,里面会根据参数调用真正的 StatmentHandler

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

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

}

为什么要这样做,而不是做一个简单工厂,返回真正的 StatementHandler

RoutingStatementHandler 中,一共返回了3中 StatementHandler ,分别是:

  • SimpleStatementHandler :简单的 Statement ,对应 JDBC 中的 SimpleStatement
  • PreparedStatementHandler : 预编译的 Statement ,对应 JDBC 中的 PreparedStatement
  • CallableStatementHandler : 存储过程 Statement , 对应 JDBC 中的 CallableStatement

看到这里,可以发现,只要知道 JDBC 中这些 Statment 的区别,就能明白这些 Handler 的区别,在 MyBatis 中,默认使用的是 PreparedStatmentHandler ,此 Statement 会预编译 SQL ,使得执行速度更快。

MyBatis 中,使用 StatmentHandler 分为3步

新建 StatmentHandler – -> prepare() –> parameterize() –> doAction()

BaseStatementHandler#prepare()

@Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    //设置错误日志
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      //交给各个子类创建statement  
      statement = instantiateStatement(connection);
      //设置超时时间
      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);
    }
  }

其中, fetchSize 是用来设置数据库一次获取的数据量

对于 MySQL (其他数据库不一定,可能是10)默认情况下,比如 select * ,数据库会直接返回所有的数据,而这个时候可能会使得 JVM OOM ,而此时则可以通过设置 fetchSize ,使得 Java 程序可以一点一点的处理。

对于其他数据库,是支持 fetchSize 的时候, fetchSize 太小可能会影响性能,因此可以通过设置此值进行调试

想要此值生效,必须开启事务

https://blog.csdn.net/seven_3306/article/details/9303979

PreparedStatementHandler#instantiateStatement()

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    //如果设置了需要获取自增长id
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        String[] keyColumnNames = mappedStatement.getKeyColumns();
        //设置JDBC需要返回id
        if (keyColumnNames == null) {
            return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
        } else {
            return connection.prepareStatement(sql, keyColumnNames);
        }
    //如果没有设置ResultSet 则使用默认的ResultSet    
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
        return connection.prepareStatement(sql);
    } else {
    //否则使用用户设置色`ResultSet`    
        return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
}

有关 ResultSetType 详细信息,可以见 JDBC 查询的三大参数

ParameterSize

ParameterSize 大概的含义就是处理参数,主要在 PreparedStatementHandler 中,用于 setString() / setInteger() 等等

@Override
public void setParameters(PreparedStatement ps) {
    //添加日志信息
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());

    //获取此行命令需要的ParamMapping
    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 {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                //使用TypeHandler处理参数
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                //调用typeHandler处理参数
                try {
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException | SQLException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

可以看到上面代码虽然比较多,但是其实就是循环处理需要的参数而已。

而对于参数分为两种:

typeHandler

这里有一个小问题,在于上面的代码中,参数始终为 parameterObject 那如果存在多个参数的情况怎么办?

参数处理

我们知道 MyBatis 的接口是通过动态代理实现的。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        } else if (method.isDefault()) {
            if (privateLookupInMethod == null) {
                return invokeDefaultMethodJava8(proxy, method, args);
            } else {
                return invokeDefaultMethodJava9(proxy, method, args);
            }
        }
    } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
    }
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);
}

以上是动态代理接口的方法,可以看见参数其实是一个数组,也可以是一个可变参数,因次不管接口中包含多少个参数, MyBatis 都会收到一个数组。

当拿到数组后, MyBatis 会结合注解进行处理:

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
        return null;
    } 
    //如果没有注解,并且参数数量为1,直接返回
    else if (!hasParamAnnotation && paramCount == 1) {
        return args[names.firstKey()];
    } 
    //否者使用HashMap进行包装
    else {
        final Map<String, Object> param = new ParamMap<>();
        int i = 0;
        for (Map.Entry<Integer, String> entry : names.entrySet()) {
            //添加key value
            //例如@Param("name")  则 key 为 name , value 为实际的值
            param.put(entry.getValue(), args[entry.getKey()]);
            // add generic param names (param1, param2, ...)
            final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
            // ensure not to overwrite parameter named with @Param
            // 同时也会添加类似 key param1  value 为实际的值到map中 
            if (!names.containsValue(genericParamName)) {
                param.put(genericParamName, args[entry.getKey()]);
            }
            i++;
        }
        return param;
    }
}

这里的代码便对应了 MyBatis 的两种参数标记方法,第一种为 @Param(name) ,第二种为 param1

值得注意的是, paramSize() 只有在 StatementPREPARED 才会有参数设置,否则 MyBatis 将不会解析对应的参数。

剩下的操作便是执行真正的操作:

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //获取Statement
    PreparedStatement ps = (PreparedStatement) statement;
    //执行
    ps.execute();
    //处理结果
    return resultSetHandler.handleResultSets(ps);
}

这里看到执行便是通过 JDBC 执行 Statement ,然后调用 ResultSetHandler 处理结果。

对于 ResultSetHandler 在下一节介绍

总结

回忆前面几篇,我们可以分析出来, MyBatis 执行过程调用的组件:

首先,通过 SqlSessionFactory 获取 SqlSession

然后,在获取 SqlSession 的过程中,会通过配置文件配置 Transaction

然后,通过以上几个参数,构建出一个 ExecutorExecutor 分为3种,对于3中不同的处理/新建 StatmentHandler 的行为

再然后, Executor 会调用 StatementHandler 各个方法,配置参数,调用 ParameterHandler 处理参数等,最后再进行执行,其中 StatementHnalder 分为3中。 STATEMENT , PREPARED , CALLBACK ,每个方法配置一个,默认为 PREPARED

最后, StatementHandler 调用 statement 执行 SQL ,然后调用 ResultSetHandler 处理结果

原文  http://dengchengchao.com/?p=1192
正文到此结束
Loading...