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处理的属性
  • 能够通过反射获取其属性(Getter / Setter)

这里有一个小问题,在于上面的代码中,参数始终为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处理结果