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

TypeHandler to Map inappropriately invoked when method has multiple parameters #135

Closed
usethe4ce opened this issue Jan 24, 2014 · 5 comments
Assignees
Labels
Milestone

Comments

@usethe4ce
Copy link

If a TypeHandler for the Map class is registered, it gets invoked inappropriately when a mapper method uses multiple parameters.

For example, a TypeHandler:

@MappedTypes(Map.class)
public class LabelsTypeHandler implements TypeHandler<Map<String, Object>> { ... }

Then these methods in the mapper interface:

@Select("SELECT #{myId};")
int echoSingle(@Param("myId") long myId);
@Select("SELECT #{myId};")
int echoMulti(@Param("myId") long myId, @Param("yourId") int yourId);

The first method, with just one parameter, succeeds, while the second method fails with a stack trace that ends with:

### Error querying database.  Cause: java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.Map
### The error may exist in org/example/persistence/SomeMapper.java (best guess)
### The error may involve org.example.persistence.SomeMapper.echoMulti-Inline
### The error occurred while setting parameters
### SQL: SELECT ?;
### Cause: java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.Map
    at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:23)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:107)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:98)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:62)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:358)
    ... 35 more
Caused by: java.lang.ClassCastException: java.lang.Long cannot be cast to java.util.Map
    at org.example.typehandler.LabelsTypeHandler.setParameter(LabelsTypeHandler.java:1)
    at org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(DefaultParameterHandler.java:77)
    at org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize(PreparedStatementHandler.java:77)
    at org.apache.ibatis.executor.statement.RoutingStatementHandler.parameterize(RoutingStatementHandler.java:58)
    at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:71)
    at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:56)
    at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:259)
    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:132)
    at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:115)
    at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:104)
    ... 42 more
@ghost ghost assigned emacarron Jan 25, 2014
@emacarron
Copy link
Member

Hi again Luke. I added a test with a handler and two params but it did not fail. Have you been able to isolate the problem? Can you create a repo with a failing test? That will be really helpful.

@emacarron
Copy link
Member

I was finally able to reproduce it. It only fails with annotations. Lets see what is happening.

@emacarron
Copy link
Member

This is an interesting chain of side effects coming indeed from a bug originated in #98

Given this scenario:
A mapper with more than one parameter:

@Select("select * from users where id = #{id}")
User getUser(@Param("id") Integer id, @Param("name") String name);

A handler for Map.class

@MappedTypes(Map.class)
public class LabelsTypeHandler implements TypeHandler<Map<String, Object>> {

This is the sequence of problems:
Parsing Stage:
1- When using a mapper with more than one parameter, MyBatis is creating a ParamMap object that extends HashMap and stores all parameters inside it. Also sets the parameterType attibute to Map.class. (wrong!) [1]
2- When binding parameters to handlers it finds the parameter #{id} and given there is a handler for the input type Map.class, it binds it to the LabelsTypeHandler (wrong!)

Execution Stage:
1- When running the statement the first thing to decide is what to pass to the typehandler: the whole input object? or one of its properties? For example if you have passed "User" as a parameter, and there is an #{id} parameter MyBatis should call getId() and pass that to the TypeHandler. But if there is a typehandler for User it will pass User. (This implies that there is just one parameter otherwise the TypeHandler will be called several times, though MyBatis does not check it)
2- The actual parameter in this case is "ParamMap". It finds no handler for it so gets the value of "id" that is a Long
3- It gets the handler bound to the mapping that happens to be "LabelsTypeHandler" and passes a Long to it what causes the ClassClassException. (crash!)

So seems that we should fix [1] by just doing what I warnend on #98: telling the truth!: parameterType is ParamMap, not Map.

Sorry for the long explanation but given this bug is complex I wanted to have the change documented.

@usethe4ce
Copy link
Author

Wow, cool!

@solutionaddicts
Copy link

I am facing the same issue. I have a nvarchar column in the database which has JSON content with key and value pairs. The mapped model has a Map field to retrieve the column, as you mentioned other column types are mapping to this handler and failing with Cause: java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Map.

FYI: I don't have @MappedTypes annotation on my handler. If I added the annotation @MappedTypes(Map.class), it has the same behavior failing with class cast exception.

The API which has this map field in the model is working fine, but when we hit different API, it's going this handler and failing with class cast exception.
at org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(DefaultParameterHandler.java:89)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.parameterize(PreparedStatementHandler.java:93)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.parameterize(RoutingStatementHandler.java:64)
at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:324)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
at com.sun.proxy.$Proxy151.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)
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.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
... 176 common frames omitted
Caused by: org.apache.ibatis.type.TypeException: Error setting non null for parameter #1 with JdbcType NVARCHAR . Try setting a different JdbcType for this parameter or a different configuration property. Cause: java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Map
at org.apache.ibatis.type.BaseTypeHandler.setParameter(BaseTypeHandler.java:55)
at org.apache.ibatis.scripting.defaults.DefaultParameterHandler.setParameters(DefaultParameterHandler.java:87)
... 194 common frames omitted
Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.util.Map
at org.apache.ibatis.type.BaseTypeHandler.setParameter(BaseTypeHandler.java:53)
... 195 common frames omitted

Mybatis version: 3.4.5

harawata added a commit to harawata/mybatis-3 that referenced this issue Jan 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants