Return HashMap in mybatis and use it as ModelAttribute in spring MVC

I want to display list of categories in my Jsp page using spring mvc @modelAttribute.

In my mapper.xml file is

<select id="selectAllCategories" resultMap="BaseResultMap">
  select id, name from categories  
</select>

In my Mapper.java class I have method

List<Map<String, String>> selectAllCategories();

I want to have a method like this:

Map<Integer, String>`selectAllCategories();

instead of List<Map<>>, is that possible?


Solution 1:

You want to get a Map<Integer,String> where the Integer is the id and the String is the name. If there were 200 categories in your table, you would want 200 entries in your map, rather than a list of 200 maps.

MyBatis can't quite do that out of the box, but you can use its facilities to do that. I see two options.

Option 1:

The first isn't quite what you asked for but is worth showing. It gives you a Map<Integer,Category> where Category is a domain object for the categories table that has id, name (and possibly other fields from the categories table). After you've created the Category domain object, this is quite easy to do in MyBatis using the @MapKey annotation:

@Select("SELECT id, name FROM categories")
@MapKey("id")
Map<Integer,Category> getAllCategories();

In your code you would then do:

MyMapper mapper = session.getMapper(MyMapper.class);
Map<Integer,Category> m = mapper.getAllCategories();

That may or may not work for your use case depending on whether whether you can extract the name as a property of the Category object.


Option 2:

To get the Map<Integer,String> you asked for, the easiest way I know is to create a class that implements the MyBatis ResultHandler interface.

Your ResultHandler will use the default hashmap of column-name => column-value that MyBatis creates and create a single master Map. Here's the code:

public class CategoryResultHandler implements ResultHandler {

  Map<Integer,String> inMap = new HashMap<Integer,String>(); 

  public Map<Integer, String> getIdNameMap() {
    return inMap;
  }

  @Override
  public void handleResult(ResultContext rc) {
    @SuppressWarnings("unchecked")
    Map<String,Object> m = (Map<String,Object>)rc.getResultObject();
    inMap.put((Integer)getFromMap(m, "id"), 
              (String)getFromMap(m, "name"));
  }

  // see note at bottom of answer as to why I include this method
  private Object getFromMap(Map<String, Object> map, String key) {
    if (map.containsKey(key.toLowerCase())) {
      return map.get(key.toLowerCase());
    } else {
      return map.get(key.toUpperCase());
    }
  }
}

The handleResult method gets called once per row in the category table. You tell MyBatis to use the ResultHandler and then extract your master map like this:

CategoryResultHandler rh = new CategoryResultHandler();
session.select("getAllCategories", rh);
Map<Integer,String> m = rh.getIdNameMap();

One of those two should work for you.

A few final notes:

  1. Why did I include the getFromMap() helper method? Because you can't always control the case of the column name in the hashmap that MyBatis returns. More details here: mybatis- 3.1.1. how to override the resultmap returned from mybatis

  2. I have working examples of these solutions in Koan26 of the mybatis-koans (which I added based on your question): https://github.com/midpeter444/mybatis-koans