Mybatis源码解析 —— 动态设置参数机制

本文深入探讨Mybatis参数解析过程,从MethodSignature转换参数到Map,详细解析DynamicSqlSource中的参数绑定,SqlSourceBuilder获取ParameterMapping,以及DefaultParameterHandler如何设置参数。通过实例分析Mybatis如何将方法参数转化为Sql查询中的参数,揭示了参数与ParameterMapping的分离设计,使得职责明确,易于单元测试。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

在上一篇文章Mybaits Sql解析过程中,笔者介绍了Mybatis是如何根据注解生成动态Sql语句的。但是,在常用的查询中,除了生成语句之外,为Sql查询语句添加参数也是非常重要的一个环节。

本篇文章将会对Mybatis参数解析,设置的全过程,进行分析。简单点来说,就是我们会围绕着一下的方法调用例子,了解Mybatis如何将方法中的参数转化为Sql中的参数。

@Select({"<script>",
            "SELECT account",
            "FROM user",
            "WHERE id IN",
            "<foreach item='item' index='index' collection='list'",
            "open='(' separator=',' close=')'>",
            "#{item}",
            "</foreach>",
            "</script>"})
    List<String> selectAccountsByIds(@Param("list") int[] ids);

Mybatis中的Sql参数设定与执行

首先,我们来通过一张简略的图片,来大概了解整个过程。

Mybatis中的参数设定过程

通过这张图,我们可以了解到几个重点。

通过MethodSignature将参数转化为Map

调用的具体方法如下

public Object getNamedParams(Object[] args) {
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {
      return args[names.firstKey()];
    } else {
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
      //首先,根据@param中注解的值,将相关参数添加到Map中
        param.put(entry.getValue(), args[entry.getKey()]);
        // 然后,再将参数的通用名,也添加到Map中
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        if (!names.containsValue(genericParamName)) {
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

通过以上的转换之后,我们在后续的函数调用中,就可以以键值对的形式来获取方法参数的值了。

DynamicSqlSource中的参数解析

在上一片文章中,我们提到过,SqlNode中的apply方法,会将我们定义在注解中的Sql,转化成带有占位符的Sql。而实际上,除了Sql的转化之外,它还会向DynamicContext中添加Binding(参数值与对应键的绑定)。

ForeachSqlNode中的apply方法

public boolean apply(DynamicContext context) {
    Map<String, Object> bindings = context.getBindings();
    //首先,通过解析器,将参数中的值解析成为一个可遍历的集合。
    //初始状态下,bindins中只有_parameter -> { 'list' -> [1,2]}
    //提取出来后的集合为[1,2]
    final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
    if (!iterable.iterator().hasNext()) {
      return true;
    }
    boolean first = true;
    applyOpen(context);
    int i = 0;
    for (Object o : iterable) {
      DynamicContext oldContext = context;
      if (first) {
        context = new PrefixedContext(context, "");
      } else if (separator != null) {
        context = new PrefixedContext(context, separator);
      } else {
          context = new PrefixedContext(context, "");
      }
      int uniqueNumber = context.getUniqueNumber();
      // Issue #709 
      if (o instanceof Map.Entry) {
        @SuppressWarnings("unchecked") 
        Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
        applyIndex(context, mapEntry.getKey(), uniqueNumber);
        applyItem(context, mapEntry.getValue(), uniqueNumber);
      } else {
        //将对应的binding添加到DynamicContext中。
        //第一次遍历时,增加到binding 为 { '__frch_index_0' -> 0 }
        applyIndex(context, i, uniqueNumber);
        //第一次遍历时,增加到binding 为 { '__frch_item_0' -> 1 }
        applyItem(context, o, uniqueNumber);
      }
      contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
      if (first) {
        first = !((PrefixedContext) context).isPrefixApplied();
      }
      context = oldContext;
      i++;
    }
    applyClose(context);
    return true;
  }

当SqlNode解析完成后,我们得到的DynamicContext中Sql如下:

 SELECT account FROM user WHERE id IN  (   #{__frch_item_0}  ,  #{__frch_item_1}  )   

而在DynamicContext中的binding,则如下所示:

'__frch_index_0' -> 0,
'__frch_item_0' -> 1,
'__frch_index_1' -> 1,
'__frch_item_0' -> 2,
'_parameter' -> { 'list' -> [1,2] , 'param1' ->[1,2] },
'_databaseID' -> null

SqlSourceBuilder中获取ParameterMapping

虽然在DynamicContext中已经有了Bindings,但是Mybatis并不会直接使用这些binding进行查询。它会从含有占位符的语句中提取ParameterMapping关系,然后再根据ParameterMapping来对参数进行设置。

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    //ParameterMappingTokenHandler,在查找到特定的token之后,对token进行处理,并且返回处理后的字符串
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    //只负责找到被#{}包围的字符串,然后交由tokenHandler进行处理
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

ParameterMappingTokenHandler

public String handleToken(String content) {
      //首先,使用token中的标志符构造ParameterMapping,然后返回"?"
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

而在这一次处理之后,我们就能够能到可以被直接执行的Sql了。

SELECT account FROM user WHERE id IN  (   ?  ,  ?  )

同时,也生成了对应的ParameterMapping(暂时忽略一些其它属性)

[
{ property: '__frch_item_0'},
{ property: '__frch_item_1'}
]

最后,在构造BoundSql时,Mybatis还会做如下的事情:

//将默认的参数,也作为BoundSql的默认参数
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    //在context中生成的binding,则会做为额外的参数,也传给boundSql
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;

参数的使用——DefaultParameterHandler

在这个类里面,我们只需要关注一个方法setParameters

public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      //parameterMapping是根据Sql中占位符逐个生成的,因此数组中的顺序也等同于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)) { 
          // 首先,从额外的参数中获取参数值,获取顺序很重要,这个与Mybatis中的issue #448有关
            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);
            value = metaObject.getValue(propertyName);
          }
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            //考虑到Java类型与Mysql类型还有一个映射关系,所以使用typeHandler进行处理
            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);
          }
        }
      }
    }
  }

到这里,参数的设定就已经完成了。

结语

在这片文章中,我们围绕着一个例子解释了Mybatis是如何进行参数设定的。在这里面,我们能够发现几个有意思的地方。

  1. 参数与ParameterMapping的分离。ParameterMapping根据解析后带占位符的Sql解析而得到,参数则始终保存在BoundSql的Parameter或者是AdditionalParameter中。这样的分离使得相互的职责更为明确,也更易于单元测试。

  2. 简单的设计。BoundSql作为执行的主体,里面只包含有Sqls,Parameter以及parameterMappings。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值