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

来源:互联网 发布:java字符串只保留汉字 编辑:程序博客网 时间:2024/05/17 08:41

前言

在上一篇文章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。

阅读全文
0 0