mybatisは@SelectProviderを使用して動的文を構築し、複数のパラメータ(@param注釈を使用しない場合)、3.4.2バージョン以降BindingException:Parameter'arg 0'

10176 ワード

最近コードがmybatisをアップグレードしたバージョン3.4.6,@SelectProviderを使用して複数のパラメータ(@param注記を使用しない場合)を構築する動的文が起動後に見つかり,クエリ時にエラーが報告される.
 org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found.

公式の例
そのためにわざわざmybatisの公式説明をめくって、次の例を見つけました.
@SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
List getUsersByName(
    @Param("name") String name, @Param("orderByColumn") String orderByColumn);

class UserSqlBuilder {

  // If not use @Param, you should be define same arguments with mapper method
  public static String buildGetUsersByName(
      final String name, final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }

  // If use @Param, you can define only arguments to be used
  public static String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
    return new SQL(){{
      SELECT("*");
      FROM("users");
      WHERE("name like #{name} || '%'");
      ORDER_BY(orderByColumn);
    }}.toString();
  }
}

テストの結果、公式コードに記述された最初の書き方(daoレイヤのbuildGetUserByNameメソッドは各パラメータの前に@Param注記を付け、内部クラスに対応するbuildGetUserByNameメソッドはパラメータ名、数をdaoレイヤの@Param注記と一致させるだけ)を付け、3.4.2バージョンまでは正常に使用できたが、3.4.2バージョンになるとエラーが報告される.
 org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found.

異常から,@param注釈を用いない場合,伝達パラメータには#{arg 0}-#{argn}または#{param 1}-#{paramn}を用いる必要があることがわかる.
エラー原因
プロジェクトが開始されると、mybatisはすべての@SelectProviderラベルを検索し、対応するtype属性に基づいて内部クラスを見つけ、providerSqlSourceオブジェクトを構築します.ここで、キーはproviderMethodArgumentName(メソッドパラメータ名配列)属性に割り当てられます.
3.4.2リリース前の割り当て方法
this.providerMethodArgumentNames = extractProviderMethodArgumentNames(m);

パラメータmは、公式の例のbuildGetUserByNameのような現在のメソッドmethodを表す.具体的な関数の実装:
  private String[] extractProviderMethodArgumentNames(Method providerMethod) {
    String[] argumentNames = new String[providerMethod.getParameterTypes().length];
    for (int i = 0; i < argumentNames.length; i++) {
      Param param = findParamAnnotation(providerMethod, i);
      argumentNames[i] = param != null ? param.value() : "param" + (i + 1);
    }
    return argumentNames;
  }

  private Param findParamAnnotation(Method providerMethod, int parameterIndex) {
    final Object[] annotations = providerMethod.getParameterAnnotations()[parameterIndex];
    Param param = null;
    for (Object annotation : annotations) {
      if (annotation instanceof Param) {
        param = Param.class.cast(annotation);
        break;
      }
    }
    return param;
  }

コードから@Paramを先に検索していることが分かるが,場合によってはParamタグの値をパラメータ名とし,ない場合は「param 1」~「paramn」をパラメータ名として用いる.providerMethodArgumentNamesの属性は「param 1」~「paramn」である.
3.4.2リリース後の割り当て方法
this.providerMethodArgumentNames = new ParamNameResolver(configuration, m).getNames();

パラメータconfigurationはmybatisの構成であり、パラメータmはmethodである
  public ParamNameResolver(Configuration config, Method method) {
    final Class>[] paramTypes = method.getParameterTypes();
    final Annotation[][] paramAnnotations = method.getParameterAnnotations();
    final SortedMap map = new TreeMap();
    int paramCount = paramAnnotations.length;
    // get names from @Param annotations
    for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
      if (isSpecialParameter(paramTypes[paramIndex])) {
        // skip special parameters
        continue;
      }
      String name = null;
      for (Annotation annotation : paramAnnotations[paramIndex]) {
        if (annotation instanceof Param) {
          hasParamAnnotation = true;
          name = ((Param) annotation).value();
          break;
        }
      }
      if (name == null) {
        // @Param was not specified.
        if (config.isUseActualParamName()) {
          name = getActualParamName(method, paramIndex);
        }
        if (name == null) {
          // use the parameter index as the name ("0", "1", ...)
          // gcode issue #71
          name = String.valueOf(map.size());
        }
      }
      map.put(paramIndex, name);
    }
    names = Collections.unmodifiableSortedMap(map);
  }

  private String getActualParamName(Method method, int paramIndex) {
    if (Jdk.parameterExists) {
      return ParamNameUtil.getParamNames(method).get(paramIndex);
    }
    return null;
  }


@Param注記を使用していないため、コードはシステムがuseActualParamName(java 8の反射を使用してメソッドパラメータ名を得る)を開いたかどうかを判断し、3.4.バージョン2以降のこの属性はデフォルトでtrueで、コードはParamName Utilを実行します.getParamNames()
public class ParamNameUtil {
  private static List getParameterNames(Executable executable) {
    final List names = new ArrayList();
    final Parameter[] params = executable.getParameters();
    for (Parameter param : params) {
      names.add(param.getName());
    }
    return names;
  }

}

エージェントのため、ここで取得した名前は「agr 0」~「agrn」であり、実際のコードで記述されたパラメータ名ではない.providerMethodArgumentNameの属性は「agr 0」~「agrn」である.
useActualParamNameをfalseに設定すると、取得したパラメータ名は「0」~「n」となります.providerMethodArgumentNameの属性は「0」~「n」である.
プロジェクトの開始後、コードクエリーを実行するとmybatisは次のコードを実行します.
  public MapperMethod(Class> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }
    public MethodSignature(Configuration configuration, Class> mapperInterface, Method method) {
      Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
      if (resolvedReturnType instanceof Class>) {
        this.returnType = (Class>) resolvedReturnType;
      } else if (resolvedReturnType instanceof ParameterizedType) {
        this.returnType = (Class>) ((ParameterizedType) resolvedReturnType).getRawType();
      } else {
        this.returnType = method.getReturnType();
      }
      this.returnsVoid = void.class.equals(this.returnType);
      this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
      this.returnsCursor = Cursor.class.equals(this.returnType);
      this.mapKey = getMapKey(method);
      this.returnsMap = this.mapKey != null;
      this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
      this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
      this.paramNameResolver = new ParamNameResolver(configuration, method);
    }

肝心なのは最後の
this.paramNameResolver = new ParamNameResolver(configuration, method);

ここでの実装は,以前にProviderSqlSourceを構築したときにproviderMethodArgumentNamesに与えられた値と同じであり,区別するときにここで得られたのはmapper内のメソッドとパラメータであり,mapperメソッドに@Param注釈が書かれているため,ここでパラメータの名前を正しく取得し,paramNameResolverに値を与えることができる.
execute()メソッドを実行する前にmybatisはmapperメソッドのパラメータ名配列とパラメータ値変換をmapに拡張します.
    Object param = method.convertArgsToSqlCommandParam(args);

argsはパラメータ値の配列です
method.converArgsToSqlCommandParam()メソッドは、次の論理を実行します.
    public Object convertArgsToSqlCommandParam(Object[] args) {
      return paramNameResolver.getNamedParams(args);
    }
 /**
   * 

* A single non-special parameter is returned without a name.
* Multiple parameters are named using the naming rule.
* In addition to the default names, this method also adds the generic names (param1, param2, * ...). *

*/ 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 param = new ParamMap(); int i = 0; for (Map.Entry entry : names.entrySet()) { 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 if (!names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; } return param; } }
で得られたmapは["param 1":"0","name":"123","param 2":","orderByColumn":""];
最終的にmybatisは次のコードを実行してmapperのメソッドとSelectProviderクラスのメソッドのパラメータの対応を完了します.
  private Object[] extractProviderMethodArguments(Map params, String[] argumentNames) {
    Object[] args = new Object[argumentNames.length];
    for (int i = 0; i < args.length; i++) {
      if (providerContextIndex != null && providerContextIndex == i) {
        args[i] = providerContext;
      } else {
        args[i] = params.get(argumentNames[i]);
      }
    }
    return args;
  }

ここでパラメータparamsは,我々が得たmapperのmap−[「param 1」:「0」,「name」:「123」,「param 2」:「」,「orderByColumn」:「」];パラメータargumentNamesは、前のSelectProviderのproviderMethodArgumentNamesで3.4.2バージョン前は「param 1」~「paramn」で3.4.2バージョン以降は「arg 0」~「argn」または「0」~「1」である.だからプログラムは3.4です.2バージョン前は3.4で正常に走ることができます.2リリース後のレポート
org.apache.ibatis.binding.BindingException: Parameter 'arg0' not found.
org.apache.ibatis.binding.BindingException: Parameter '0' not found.

解決策
解決策は簡単で,SelectProviderメソッドのパラメータ名対応にmapperメソッドと同様の@Param注釈を加える.
本文は等風de帆-29、ParamNameResolverパラメータ解析と彼岸の饅頭のmybatis伝の複数のパラメータ(@param注釈を使用しない場合)を参考し、3.4.バージョン2以降で#{0}-#{n}を使用して発生したパラメータバインド異常とsettingsプロパティのuseActualParamNameの役割.