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 ofid
intest
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?