MyBatis 入门 (参数绑定特别版)

MyBatis 接口参数与 XML SQL 参数实现动态拼接详解

共分为五个步骤:

  1. 接口参数解析
  2. 根据参数名获取参数值
  3. Collection、List、Map 类型参数再次封装
  4. 接口参数与动态上下文对象绑定
  5. 参数与 SQL 拼接
  6. 补充

接口参数解析

代码流程如下:在这里插入图片描述
详情代码段:

ParamNameResolver.class

/**
  * @Param	config	MyBatis 的全局配置对象
  *
  *	@Param	method 当前被调用的接口方法,由上一级的动态代理类传入
  */
public ParamNameResolver(Configuration config, Method method) {
    // 获取方法的参数类型
    final Class<?>[] paramTypes = method.getParameterTypes();
    // 获取方法的注解
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap<Integer, String> map = new TreeMap<>();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      ...
      String name = null;
      // 如果参数前使用了 @Param 注解,则获取到参数名
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      // 如果参数未使用 @Param 注解
      if (name == null) {
        // 请留意此处....
        // 则使用默认的参数名 useActualParamName 属性默认为 true ,则参数名的格式为:arg0,arg1...
        if (config.isUseActualParamName()) {
          name = getActualParamName(method, paramIndex);
        }
        if (name == null) {
          // 如果 useActualParamName 被配置为 false ,则参数名为索引名,如:"0", "1", ...
          name = String.valueOf(map.size());
        }
      }
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

// 最终 names 的内容如下:
// Map 类型:paramIndex --》 paramName

分析 ParamNameResolver 的构造器方法中可以得出以下结论:

  • 当我们使用 @Param 注解时,参数名为注解中设置的值
  • 未使用注解时,未设置全局的 useActualParamName 配置时,useActualParamName 默认配置为 true,则参数名会被设置为:arg0,arg1… 格式
  • 未使用注解时,设置全局的 useActualParamName = false 后,参数名会被设置为:“0”, “1”, … 格式

根据参数名获取参数值:

下图为获取具体参数值调用流程:
在这里插入图片描述
可以看到,分支2 的调用,最终也是调用的 ParamNameResolver 类中的方法进行参数的获取:

ParamNameResolver.class

/**
  *	@Param args 接口中传入的参数值,数组形式
  *
  * @return 单个参数,且没有使用 @Param 注解时,直接返回参数值;多个参数时,返回:参数索引--》参数值 类型的 Map
  */
public Object getNamedParams(Object[] args) {
    // 获取接口参数的长度
    final int paramCount = names.size();
    if (args == null || paramCount == 0) {
      return null;
    } else if (!hasParamAnnotation && paramCount == 1) {	
      // 如果只有一个参数,且没有使用 @Param 注解,则默认通过索引下标获取第一个参数值
      return args[names.firstKey()];
    } else {
      // 如果是多个参数,或者是启用了 @Param 注解
      final Map<String, Object> param = new ParamMap<>();
      int i = 0;
      for (Map.Entry<Integer, String> entry : names.entrySet()) {
        // entry.getValue() 为参数名,entry.getKey() 为参数索引,args[entry.getKey()] 为参数值
        // 如果接口中参数使用了 @Param ,则此处 param 中存入的将是:interface_@Param_value--》args[?]
        // 如果接口中参数未使用 @Param ,且是多个参数,则 param 中存入的是:args0--》args[0],args1--》args[1]...
        param.put(entry.getValue(), args[entry.getKey()]);
        // 并向 param 中添加一组 通用的参数键值: param1 --》args[0],param2 --》args[1]....
        final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
        if (!names.containsValue(genericParamName)) {
          // 如果接口中定义的参数没有 paramIndex 格式的,则在此处会将参数在添加一次到 param 中,这次的 key 是 paramIndex 格式的
          param.put(genericParamName, args[entry.getKey()]);
        }
        i++;
      }
      return param;
    }
  }

Collection、List、Map 类型参数再次封装

调用流程如下:
在这里插入图片描述

// 如果你的 接口参数使用了 @Param 注解,则参数不会被该方法处理
private Object wrapCollection(final Object object) {
	// 如果 参数类型 是 Collection ,则给 Map 中添加一个 key 为 collection 类型的键值,同时也是 List 类型,再添加一个  key 为 list 键值
    // 可以使用 MyBatis 的动态标签通过 key 值 collection/list/array 来引用此处封装的 集合/Map 类型参数
    if (object instanceof Collection) {
      StrictMap<Object> map = new StrictMap<>();
      map.put("collection", object);
      if (object instanceof List) {
        map.put("list", object);
      }
      return map;
    } else if (object != null && object.getClass().isArray()) {
      // 如果 参数类型 是 Array 类型,则添加一个 key 为 array 的键值
      StrictMap<Object> map = new StrictMap<>();
      // 此处与 map.put("list", object); 同理  
      map.put("array", object);
      return map;
    }
    return object;
  }

接口参数与动态上下文对象绑定

接口参数绑定的话,我们使用 MyBatis 的过程中会经常使用与动态标签配合的方式,这里先只介绍动态标签类型的参数绑定过程,流程图如下:
在这里插入图片描述
具体我们来看 DynamicSqlSource 类的 getBoundSql(Object parameterObject) 这个方法:

DynamicContext.class
    
public BoundSql getBoundSql(Object 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);
    // 参数转存 到 metaParameters 中
    // metaParameters 的值最终是如下格式:
    //					_parameter -> parameterObject
    //					_databaseId -> null(根据实际 XML sql 是否设置 databaseId 值来决定)
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }    
    
// 对应上图 分支4 中的 --》 XML 中当前 SQL 使用 动态标签
// 当进入该方法时,parameterObject 参数如果为 Collection/Map 类型时,则已经经过 wrapCollection(final Object object) 封装过一次了
public DynamicContext(Configuration configuration, Object parameterObject) {
    if (parameterObject != null && !(parameterObject instanceof Map)) {
      MetaObject metaObject = configuration.newMetaObject(parameterObject);
      bindings = new ContextMap(metaObject);
    } else {
      bindings = new ContextMap(null);
    }
    // PARAMETER_OBJECT_KEY 值为 “_parameter”
    // parameterObject 是在 wrapCollection(final Object object) 中被组装过的
    bindings.put(PARAMETER_OBJECT_KEY, parameterObject);
    bindings.put(DATABASE_ID_KEY, configuration.getDatabaseId());
  }

参数与 SQL 拼接

流程图如下:
在这里插入图片描述

DefaultParameterHandler.class

@Override
  public void setParameters(PreparedStatement ps) {
    ....
    // 获取接口参数对象集合
    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)) { 
            // 使用参数名在 metaParameters 中拿到 对应的值
            // metaParameters 存在两组值 :key值分别为 _parameter 和 _databaseId 
            value = boundSql.getAdditionalParameter(propertyName);
          }...else {
            // 从当前对象的 parameterObject 中拿到对应 propertyName 的值
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          try {
            // SQL 中对应参数名更换为 value 值
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          }....
        }
      }
    }
  }

参数拼接的方法为:

ClientPreparedQueryBindings.class

/**
  * @Param paramIndex 参数索引
  * @Param val 参数值
  * @Param type 设置该参数时,Mysql 中设置的数据类型 
  *
  */
public synchronized final void setValue(int paramIndex, String val, MysqlType type) {
    byte[] parameterAsBytes = StringUtils.getBytes(val, this.charEncoding);
    setValue(paramIndex, parameterAsBytes, type);
}

到这一步,MyBatis 就完成了接口参数与 XML SQL 的拼接替换,接下来要做的便是将处理过后的 SQL 转为字节流,通过一个 BufferedOutputStream 类型的对象输出到 MySQL 的服务器端了。


补充

在刚开始使用 MyBatis 框架时,肯定都碰到过一下这个异常:

org.apache.ibatis.binding.BindingException: Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]

出现这个异常的原因是因为接口方法的参数是一个以上,并且没有通过使用 @Param 注解指定参数名

来看一下这个信息,提示 参数 ‘id’ 没有找到,可用的参数有 : arg1,arg0,param1,param2 这几个,那么,这四个可用参数是在哪一步被放到可用参数列表的呢?

在该篇博客的正文中 **根据参数名获取参数值:**部分关于 ParamNameResolver 类的 getNamedParams 方法的源码注解中,有如下一段:

for (Map.Entry<Integer, String> entry : names.entrySet()) {
    // entry.getValue() 为参数名,entry.getKey() 为参数索引,args[entry.getKey()] 为参数值
    // 如果接口中参数使用了 @Param ,则此处 param 中存入的将是:interface_@Param_value--》args[?]
    // 如果接口中参数未使用 @Param ,且是多个参数,则 param 中存入的是:args0--》args[0],args1--》args[1]...
    param.put(entry.getValue(), args[entry.getKey()]);
    // 并向 param 中添加一组 通用的参数键值: param1 --》args[0],param2 --》args[1]....
    final String genericParamName = GENERIC_NAME_PREFIX + String.valueOf(i + 1);
    if (!names.containsValue(genericParamName)) {
    // 如果接口中定义的参数没有 paramIndex 格式的,则在此处会将参数在添加一次到 param 中,这次的 key 是 paramIndex 格式的
    param.put(genericParamName, args[entry.getKey()]);
    }
    i++;
}

在该方法中会给多个参数,并且为使用 @Param 注解的接口参数进行两遍赋值;

  • 第一遍赋值 :key 的格式是从 names 中拿到的,names 是在 接口参数解析一节中的 ParamNameResolver 类的 ParamNameResolver 方法中被赋值,赋值关键过程如下:
// 如果参数未使用 @Param 注解
if (name == null) {
    // 请留意此处....
    // 则使用默认的参数名 useActualParamName 属性默认为 true ,则参数名的格式为:arg0,arg1...
    if (config.isUseActualParamName()) {
    	name = getActualParamName(method, paramIndex);
    }
    if (name == null) {
        // 如果 useActualParamName 被配置为 false ,则参数名为索引名,如:"0", "1", ...
        name = String.valueOf(map.size());
    }
}
...
names = Collections.unmodifiableSortedMap(map);
  • 第二遍赋值:
if (!names.containsValue(genericParamName)) {
    // 如果接口中定义的参数没有 paramIndex 格式的,则在此处会将参数在添加一次到 param 中,这次的 key 是 paramIndex 格式的
    param.put(genericParamName, args[entry.getKey()]);
}

两边赋值过后,可用参数名的列表为:[arg1, arg0, param1, param2] ,而 ‘id’ 不在可用参数列表中,所以就会产生参数没有被找到的异常;要解决该异常,推荐通过给多个接口参数分别增加 @Param 注解来指定参数名,同时,日常开发中,当接口参数为多个是,也推荐使用 @Param 注解来进行参数绑定;虽然 MyBatis 会自动设置一些参数名,不过往往不利于代码的理解。


版权声明:本文为shizhiqian0021原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。