MyBatis 中使用 JSONObject 处理一对多结果

# 问题

上一文介绍了 JSONObject 接受 MyBatis 的结果集的简单用法,但是在处理一对多的情况时,单纯的JSONObject就不好使了。

比如要查询一个角色下的多个用户,resultMap如下定义

1
2
3
4
5
6
7
8
<resultMap id="roleMap" type="com.alibaba.fastjson.JSONObject">
    <id column="roleId" property="roleId"/>
    <result column="roleName" property="roleName"/>
    <collection property="users" ofType="com.alibaba.fastjson.JSONObject">
        <id column="userId" property="userId"/>
        <result column="nickname" property="nickname"/>
    </collection>
</resultMap>

期望查出来的users 属性对应着一个数组,

然而实际查出来只是一个对象,只有一条数据。

# 解决方案

只需要建一个实体类继承 JSONObject ,里面有你要的集合类型的成员变量,就足够了。

比如我建的 One2Many 类:

1
2
3
public class One2Many extends JSONObject {
    private List<JSONObject> users;
}

然后xml改为

1
2
3
4
5
6
7
8
<resultMap id="roleMap" type="com.heeexy.example.util.model.One2Many">
    <id column="roleId" property="roleId"/>
    <result column="roleName" property="roleName"/>
    <collection property="users" ofType="com.alibaba.fastjson.JSONObject">
        <id column="userId" property="userId"/>
        <result column="nickname" property="nickname"/>
    </collection>
</resultMap>

是不是非常简单?

更棒的是,这个 One2Many 类是可以复用的,里面再添加其它的成员变量就 OK 了。而且 Dao 层不需要改动,外面正常的还是用 JSONObject 就可以了。

# 原理

MyBatis 在处理嵌套结果的时候,会判断这个属性的类型,如果是集合,就会初始化一个集合来接收这个属性,否则就只是一个普通的 Object 了。

什么,不满意这个答案?那就拿出源码来吧!

首先我们直接看到最底层判断这个属性是不是集合的这段源码:

DefaultResultSetHandler.instantiateCollectionPropertyIfAppropriate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
  final String propertyName = resultMapping.getProperty();
  //先去拿在metaObject已经存好的这个属性值,我们这里以users属性为例
  Object propertyValue = metaObject.getValue(propertyName);
  if (propertyValue == null) {
    //在这里拿你的users的java类型,由于我们使用的是JSONObject,这里的类型都会返回Object,如果是One2Many,这里就会拿到List类型
    Class<?> type = resultMapping.getJavaType();
    if (type == null) {
      type = metaObject.getSetterType(propertyName);
    }
    try {
      //判断属性类型是不是集合,如果是结合才会初始化一个集合的值返回到下一步,否则都会返回null
      if (objectFactory.isCollection(type)) {
        propertyValue = objectFactory.create(type);
        metaObject.setValue(propertyName, propertyValue);
        return propertyValue;
      }
    } catch (Exception e) {
      throw new ExecutorException("Error instantiating collection property for result '" + resultMapping.getProperty() + "'.  Cause: " + e, e);
    }
  } else if (objectFactory.isCollection(propertyValue.getClass())) {
    //propertyValue不为空的情况,即我们的JSONObject里已经塞入了users属性,即使是这样,MyBatis还是要求你塞入的users属性必须是集合,才返回到下一步,否则还是会返回null到下一步。
    return propertyValue;
  }
  return null;
}

从上面这段代码我们就知道 MyBatis 确实有做这个判断,你定义的 users 属性到底是不是集合类型,

  1. 如果是并且没有初始化好的话,就帮你初始化一个集合到下一步,
  2. 如果已经初始化好了(通常这时候就是已经塞入了几个 user 对象了),就直接返回这个 users 的值到下一步
  3. 如果不是集合类型,就返回 null 到下一步。

那么下一步到底是干啥呢?正常情况下应该就是继续往 users 集合里添加元素吧。

DefaultResultSetHandler.linkObjects

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
  final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
  if (collectionProperty != null) {
    //如果上一步返回来的不是null,那就向这个集合里添加元素
    final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
    targetMetaObject.add(rowValue);
  } else {
    //上一步返回了null,那就直接把这个属性赋上本值,就是这种情况导致了上面的第二张图片的情况,我们的users变成了一个对象,而不是想要的数组。
    metaObject.setValue(resultMapping.getProperty(), rowValue);
  }
}