Mybatis源码解析:sql参数处理(3)

本文深入解析Mybatis中#{}参数处理的源码,从SqlSource的选择到PreparedStatement的使用。讲解了DynamicSqlSource如何处理${},RawSqlSource如何预处理#{XXX}为?,并分析了包含两种占位符的特殊情况。总结了Mapper XML文件解析过程和执行时的参数动态替换机制。

入参#{}的解析

那么如果是#{}该怎么处理呢?

<select id="get" resultType="com.entity.User">
      select * from user where id = #{id}
  </select>
List<User> get(Integer id);

由上文得知,由于没有${},那么SqlSource就会变成RawSqlSource。在创建RawSqlSource的时候,在构造方法中就会对#{}解析。

RawSqlSource的构造方法。

public RawSqlSource(Configuration configuration, SqlNode rootSqlNode, Class<?> parameterType) {
    this(configuration, getSql(configuration, rootSqlNode), parameterType);
  }

  public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>());
  }

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);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这里用的hander是ParameterMappingTokenHandler,它的作用是将#{XXX}替换成 

ParameterMappingTokenHandler.handleToken

public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

这时sql就变成了select * from user where id = ?,到这里还只是解析配置文件。在具体执行方法时也要调用getBoundSql方法将参数进行赋值

//RawSqlSource.getBoundSql
  public BoundSql getBoundSql(Object parameterObject) {
    return sqlSource.getBoundSql(parameterObject);
  }

StaticSqlSource.getBoundSql,最后调用BoundSql的构造方法,将sql语句,入参等传入

public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

之后就要创建数据库连接,进行查询了。回到这个方法SimpleExecutor.prepareStatement。回顾一下,这是创建StatementHandler后做的一些连接数据库的准备操作。

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  //获取jdbc数据库连接
  Connection connection = getConnection(statementLog);
  //一些准备工作,初始化Statement连接
  stmt = handler.prepare(connection, transaction.getTimeout());
  //使用ParameterHandler处理入参
  handler.parameterize(stmt);
  return stmt;
}

我们先进入这个方法PreparedStatementHandler.parameterize。

为什么是PreparedStatementHandler之前也说过,因为语句的默认类型是PREPARED, 还有其他的类型如果是CALLABLE,对应CallableStatementHandler,STATEMENT对应SimpleStatementHandler。可以用参数statementType进行设置。

@Override
public void parameterize(Statement statement) throws SQLException {
  parameterHandler.setParameters((PreparedStatement) statement);
}

DefaultParameterHandler.setParameters.

@Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //boundSql用来解析我们的sql语句,parameterMappings是我们传入的参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        //这里第一个参数就是id
        ParameterMapping parameterMapping = parameterMappings.get(i);
        //mode属性允许能指定IN,OUT或INOUT参数。如果参数的 mode 为 OUT 或 INOUT,将会修改参数对象的属性值,以便作为输出参数返回。
        //#{id}默认mode为OUT
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          //这里是boundsql中的额外参数,可以使用拦截器添加,例子放在下文
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
            //如果类型处理器中有这个类型,那么直接赋值就行了,例如这里是Integer类型,类型处理器是有的
            //那么直接赋值
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
          //如果不是的会转化为元数据进行处理,metaObject元数据可以理解为用来反射的工具类,可以处理参数的get,set
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          //获取类型处理器
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          //获取数据库类型
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            //使用不同的类型处理器向jdbc中的PreparedStatement设置参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

value如果是空的那么就直接设置为jdbc的空类型,不为空调用具体的类型处理器。

BaseTypeHandler.setParameter。该类是所有typeHandler的父类.如果不为空调用setNonNullParameter,该方法时抽象的,由具体的子类实现。这里使用的是一个相当于路由的的子类UnknownTypeHandler,这个子类可以根据传入的类型,再去找到具体的类型处理器,例如IntegerTypeHander.

@Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      if (jdbcType == null) {
        throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
      }
      try {
        ps.setNull(i, jdbcType.TYPE_CODE);
      } catch (SQLException e) {
        throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
              + "Cause: " + e, e);
      }
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
              + "Try setting a different JdbcType for this parameter or a different configuration property. "
              + "Cause: " + e, e);
      }
    }
  }

UnknownTypeHandler.setNonNullParameter

public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    handler.setParameter(ps, i, parameter, jdbcType);
  }

UnknownTypeHandler.resolveTypeHandler这个方法根据传入的参数类型,找到具体的TypeHandler

private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
    TypeHandler<?> handler;
    if (parameter == null) {
      handler = OBJECT_TYPE_HANDLER;
    } else {
      handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
      // check if handler is null (issue #270)
      if (handler == null || handler instanceof UnknownTypeHandler) {
        handler = OBJECT_TYPE_HANDLER;
      }
    }
    return handler;
  }

例如如果这个参数是id,Integer类型,那么就会找到IntegerTypeHandler

//IntegerTypeHandler
  public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setInt(i, parameter);
  }

最后还是使用jdbc的PreparedStatement处理参数。

附:自定义的拦截器用来加入参数。

@Intercepts({
  @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,Integer.class})
})
public class MyInterceptor implements Interceptor {
  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    StatementHandler bs = (StatementHandler) invocation.getTarget();
    BoundSql boundSql = bs.getBoundSql();
    boundSql.setAdditionalParameter("id","1");
    return invocation.proceed();
  }
}

例4 ${}和#{}都存在的情况

如果是都存在的情况呢?

<select id="findUserByIdAndName" resultType="com.entity.User">
        select * from user where id = ${id} AND name = #{name}
    </select>
List<User> findUserByIdAndName(@Param("id") Integer id, @Param("name") String name);

结合上文的分析,由于存在${},所以选择的DynamicSqlSource。

DynamicSqlSource.getBoundSql。这个方法上文分析到了rootSqlNode.apply(context);会将${}替换成具体参数。我们接着分析。

public BoundSql getBoundSql(Object parameterObject) {
    //parameterObject中有我们方法传入的参数
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //解析${}并替换成具体的值
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    //这里又进行了一次解析
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

解析#{},并将其替换成?

//RawSqlSource.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);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

这样我们的语句就变成了select * from user where id = 1 AND name = ?,然后调用sqlSource.getBoundSql

//StaticSqlSource.getBoundSql
  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

最后的处理方式与例3相同,使用jdbc自带的PreparedStatement进行参数处理。

小结

当我们在解析mapper.xml文件时,就会将sql进行第一遍的解析,将其中的全局变量替换成具体的值。

接着进行第二遍的解析,选择不同的SqlSource。这一边的解析不改变语句中的sql内容。

如果语句中包含${},就选择DynamicSqlSource,等待具体执行sql的时候再做处理.如果仅包含#{}类型的,就选择RawSqlSource。RawSqlSource在创建的时候就会有进行一轮的解析,将语句中的#{XXX}替换为 ?(问号)

之后在执行具体的语句才动态的替换,如果之前选择的是DynamicSqlSource,那么进行两次的解析,第一次将${}替换成具体值,第二次解析#{},使用jdbc的PreparedStatement处理。如果选择的是RawSqlSource,那么这条语句就只有#{},直接用PreparedStatement处理。

可以发现,无论什么类型的sql都会被解析了4次。

                                                                            需要更多教程,微信扫码即可

                                                                                  

                                                                                         👆👆👆

                                                        别忘了扫码领资料哦【高清Java学习路线图】

                                                                     和【全套学习视频及配套资料】
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值