Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow referencing single parameter using actual method argument name when compile with -parameters #1237

Closed
zerda opened this issue Mar 29, 2018 · 10 comments · Fixed by #1856
Assignees
Labels
enhancement
Milestone

Comments

@zerda
Copy link

zerda commented Mar 29, 2018

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.
  2. Reference this parameter in sql.
  3. Compile it with -parameters.
  4. 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.

@kazuki43zoo
Copy link
Member

kazuki43zoo commented Mar 29, 2018

@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;
+  }
}

@harawata
Copy link
Member

harawata commented Mar 29, 2018

@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.

@zerda
Copy link
Author

zerda commented Mar 30, 2018

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);
}

@kazuki43zoo
Copy link
Member

kazuki43zoo commented Mar 30, 2018

@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?

@harawata
Copy link
Member

harawata commented Apr 6, 2018

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

@xtuer
Copy link

xtuer commented Apr 10, 2018

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

@harawata
Copy link
Member

harawata commented May 15, 2018

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.

kazuki43zoo added a commit to kazuki43zoo/mybatis-3 that referenced this issue Mar 14, 2020
@kazuki43zoo kazuki43zoo self-assigned this Mar 14, 2020
@kazuki43zoo kazuki43zoo added the enhancement label Mar 14, 2020
@kazuki43zoo
Copy link
Member

kazuki43zoo commented Mar 15, 2020

@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.

kazuki43zoo added a commit to kazuki43zoo/mybatis-3 that referenced this issue Mar 19, 2020
kazuki43zoo added a commit to kazuki43zoo/mybatis-3 that referenced this issue Mar 19, 2020
kazuki43zoo added a commit to kazuki43zoo/mybatis-3 that referenced this issue Mar 19, 2020
kazuki43zoo added a commit to kazuki43zoo/mybatis-3 that referenced this issue Mar 21, 2020
@kazuki43zoo kazuki43zoo added this to the 3.5.5 milestone Mar 21, 2020
kazuki43zoo added a commit that referenced this issue Mar 21, 2020
Allow using actual argument name as bind parameter on a single collection
@kazuki43zoo kazuki43zoo changed the title Referencing parameter failed when compile with -parameters Allow referencing single parameter using actual method argument name when compile with -parameters Mar 21, 2020
@kazuki43zoo
Copy link
Member

kazuki43zoo commented Mar 21, 2020

@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!

@brahmasuresh
Copy link

brahmasuresh commented Dec 13, 2020

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 :
{ call POLL_FILE_TRANSMISSION ( #{in_trans_type, jdbcType=VARCHAR, mode=IN}, #{o_message_data_array,jdbcType=ARRAY,typeHandler=ObjFileTransPollArrayTypeHandler,jdbcTypeName=OBJ_FILE_TRANS_POLL_ARRAY, mode=OUT}, #{o_batch_size, jdbcType=INTEGER, mode=OUT} ) }
Can you please do the needful?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement
Projects
None yet
5 participants