MyBatis version

3.4.6

Database vendor and version

hsqldb

Test case or example project

https://github.com/zerda/mybatis-3/tree/param-name-resolve/src/test/java/org/apache/ibatis/submitted/param_name_resolve

Steps to reproduce

See test case. 1. Create a mapper function with only one parameter, no annotation. 1. Reference this parameter in sql. 1. Compile it with -parameters. 1. Call this function, get the result.

Expected result

Get the correct result 3.

Actual result

An Exception raised.

org.apache.ibatis.binding.BindingException: 
    Parameter 'ids' not found. Available parameters are [collection, list]

Quick debug

At ParamNameResolver.getNamedParams, it may not correctly handle the case when parameter come with name, but not annotated.

Comment From: kazuki43zoo

@zerda Thanks for your reporting!

Probably, we can fix this issue as follow: However, I think it is a very ad-hoc solution... (and logic is duplicate with DefaultSqlSession#wrapCollection)

@harawata WDYT?

public class ParamNameResolver {
  // ...
  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()];
+      return wrapCollection(args[names.firstKey()], names.get(0));
    } else {
      final Map<String, Object> param = new ParamMap<Object>();
      int i = 0;
      for (Map.Entry<Integer, String> 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;
    }
  }

+  private Object wrapCollection(final Object object, final String name) {
+    if (object instanceof Collection) {
+      ParamMap<Object> map = new ParamMap<Object>();
+      map.put(name, object);
+      map.put("collection", object);
+      if (object instanceof List) {
+        map.put("list", object);
+      }
+      return map;
+    } else if (object != null && object.getClass().isArray()) {
+      ParamMap<Object> map = new ParamMap<Object>();
+      map.put(name, object);
+      map.put("array", object);
+      return map;
+    }
+    return object;
+  }
}

Comment From: harawata

@kazuki43zoo ,

How about making the new method public static and referencing it from org.apache.ibatis.session.defaults.DefaultSqlSession to avoid duplicated logic? Also, there should be a check for useActualParamName setting.

Comment From: zerda

Thank you for the quick response.

I see the solution is specified with collection and array type, it may not working when the first parameter is primitive or other class.

<mapper>
  <select id="getUserName" resultType="String">
    select u.name
    from users u
    <where>
      <if test="id != null">
        u.id = #{id}
      </if>
    </where>
    limit 1
  </select>
</mapper>
interface Mapper {
  String getUserName(Integer id);
}

Comment From: kazuki43zoo

@zerda thanks for quick reaction.

I've tried it, ognl error occurred as follow:

org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'id' in 'class java.lang.Integer'
### Cause: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'id' in 'class java.lang.Integer'

    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:142)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:78)
    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:86)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at org.apache.ibatis.submitted.param_name_resolve.$Proxy6.getUserName(Unknown Source)
    at org.apache.ibatis.submitted.param_name_resolve.ParamNameResolveTest.a(ParamNameResolveTest.java:67)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'id' in 'class java.lang.Integer'
    at org.apache.ibatis.reflection.Reflector.getGetInvoker(Reflector.java:419)
    at org.apache.ibatis.reflection.MetaClass.getGetInvoker(MetaClass.java:164)
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.getBeanProperty(BeanWrapper.java:162)
    at org.apache.ibatis.reflection.wrapper.BeanWrapper.get(BeanWrapper.java:49)
    at org.apache.ibatis.reflection.MetaObject.getValue(MetaObject.java:122)
    at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextMap.get(DynamicContext.java:94)
    at org.apache.ibatis.scripting.xmltags.DynamicContext$ContextAccessor.getProperty(DynamicContext.java:108)
    at ognl.OgnlRuntime.getProperty(OgnlRuntime.java:2695)
    at ognl.ASTProperty.getValueBody(ASTProperty.java:114)
    at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)
    at ognl.SimpleNode.getValue(SimpleNode.java:258)
    at ognl.ASTNotEq.getValueBody(ASTNotEq.java:50)
    at ognl.SimpleNode.evaluateGetValueBody(SimpleNode.java:212)
    at ognl.SimpleNode.getValue(SimpleNode.java:258)
    at ognl.Ognl.getValue(Ognl.java:493)
    at ognl.Ognl.getValue(Ognl.java:457)
    at org.apache.ibatis.scripting.xmltags.OgnlCache.getValue(OgnlCache.java:46)
    at org.apache.ibatis.scripting.xmltags.ExpressionEvaluator.evaluateBoolean(ExpressionEvaluator.java:32)
    at org.apache.ibatis.scripting.xmltags.IfSqlNode.apply(IfSqlNode.java:34)
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:33)
    at org.apache.ibatis.scripting.xmltags.TrimSqlNode.apply(TrimSqlNode.java:55)
    at org.apache.ibatis.scripting.xmltags.MixedSqlNode.apply(MixedSqlNode.java:33)
    at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:41)
    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:292)
    at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:81)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149)
    ... 29 more

Probably, a workaround to fix this behavior is as follows:

  • Add @Param("id") on mapper method argument
  • Change to value instead of id in test expression
- <if test="id != null">
+ <if test="value != null">

Probably, it is not easy to fix this issue while keeping a backward compatibility. @harawata WDYT?

Comment From: harawata

Not sure. I'll look into it when I have time.

Comment From: xtuer

I uploaded a Unit Test to reproduce the issue, which can be accessible via https://github.com/xtuer/MyBatis-Issue-Unittest.git

Comment From: harawata

Sorry for the belated reply.

The original report is about a statement with a single List parameter. In this case, MyBatis assigns two names "list" and "collection" to the parameter implicitly. And I thought it was OK to assign another name (i.e. the actual parameter name) in addition to the implicit names.

But this comment is about a statement with single non-list parameter. In this case, assigning the actual name to the parameter will break backward compatibility. For example, when the single parameter is a JavaBean, users can reference its properties directly (e.g. #{prop1} instead of #{beanName.prop1}).

I might have answered @xtuer 's case somewhere, but #{} is evaluated by MyBatis whereas ${} or test attribute of <if /> is evaluated by OGNL. When there is only one parameter and it's a simple type (not a bean), MyBatis allows you to reference the parameter in any name, but OGNL requires you to reference it with _parameter.

Comment From: kazuki43zoo

@zerda

About a your additional comment, Since 3.5.2 we allow any variable name on OGNL expression when specify single value object as parameter object via gh-1487.

Comment From: kazuki43zoo

@zerda @xtuer @tokuhirom

We've fixed via #1487(already released at 3.5.2) and #1856. You can try this enhancement via 3.5.5-SNAPSHOT deployed on OSS sonatype snapshot repository.

https://github.com/mybatis/mybatis-3/wiki/Maven

Thanks!

Comment From: brahmasuresh

Hi Team,

How to convert oracle object to java object using typehandler which we received as OUT parameter from Stored procedure using mybatis, java, sprinboot. SP: Procedure Poll_File_Transmission (in_trans_type IN btps.transmission_header.th_trans_type%type, o_message_data_array OUT btps.Obj_File_Trans_Poll_Array, o_batch_size OUT NUMBER) IS

CURSOR pension_pmt_cur IS SELECT th_admin_id admin_id,

             FROM btps.bank_account,
                  btps.address baddr,
                  cltinfo.countries
            WHERE ba_admin_id = th_admin_id
              AND baddr.addr_country_code = co_country_code(+)
    --      
-- All done.

END Poll_File_Transmission;

Response:

2020-12-13 15:23:47.089 INFO 1216 --- [pool-2-thread-1] c.p.h.s.b.d.PollFileTransmissionImpl : Poll File Transmission procedure call completed for PMT size======>100 2020-12-13 15:23:47.089 INFO 1216 --- [pool-2-thread-1] c.p.h.s.b.d.PollFileTransmissionImpl : procedure call completed for objFileTransPollData::params_PMT 2020-12-13 15:23:47.089 INFO 1216 --- [pool-2-thread-1] c.p.h.s.b.d.PollFileTransmissionImpl : callPollFileTransmission Response[ ] ==> Here i should get as List of Objects

mapping in xml : Can you please do the needful?