乐趣区

Mybatis之结果处理器

前言

在上文 Mybatis 之方法如何映射到 XML 中讲到需要实例化 SqlCommand 和 MethodSignature 两个类,在 MethodSignature 初始化的时候有一个 resultHandlerIndex 的参数用来指定是否设置了 ResultHandler 参数,本文将重点 ResultHandler 如何使用,分析如何触发的以及如何自定义结果处理器。

使用结果处理器

首先看一下 Mybatis 提供的结果处理器接口类 ResultHandler:

public interface ResultHandler<T> {void handleResult(ResultContext<? extends T> resultContext);

}

还是比较简单的,就只有一个 handleResult 的方法,方法参数是 ResultContext 里面存放了正常执行 sql 的结果,这里的结果处理器其实就是对结果进行二次加工处理;Mybatis 提供了两个默认的处理器分别是:DefaultResultHandler 和 DefaultMapResultHandler,分别处理 list 和 map,下面看一下是如何使用这两个处理器的:

    public static void selectHandler(SqlSession session, Configuration configuration) {BlogMapper mapper = session.getMapper(BlogMapper.class);
        //DefaultResultHandler 内置结果处理器
        DefaultResultHandler defaultHandler = new DefaultResultHandler();
        mapper.selectBlogsByHandler("zhaohui", defaultHandler);
        System.out.println(defaultHandler.getResultList());

        //DefaultMapResultHandler 内置结果处理器
        DefaultMapResultHandler<Long, Blog> defaultMapResultHandler = new DefaultMapResultHandler<Long, Blog>("id",
                configuration.getObjectFactory(), configuration.getObjectWrapperFactory(),
                configuration.getReflectorFactory());
        mapper.selectBlogsByHandler("zhaohui", defaultMapResultHandler);
        System.out.println(defaultMapResultHandler.getMappedResults());
    }

上面的实例分别使用了两个内置处理器,分别处理获取到的相同结果;map 处理器指定了 id 作为 key;下面再看一下 BlogMapper 中的 selectBlogsByHandler:

public void selectBlogsByHandler(String author, ResultHandler handler);

<select id="selectBlogsByHandler" parameterType="string" resultType="blog">
        select * from blog where author = #{author}
</select>

上面唯一要注意的点就是 selectBlogsByHandler 的返回值 void 类型,不然结果处理器是不会触发处理结果的,这个后面再分析为什么;看一下输出结果:

[Blog [id=159, title=hello java, author=zhaohui, content=hello java666], Blog [id=160, title=hello java, author=zhaohui, content=hello java666]]
{160=Blog [id=160, title=hello java, author=zhaohui, content=hello java666], 159=Blog [id=159, title=hello java, author=zhaohui, content=hello java666]}

处理流程

首先我们需要了解结果处理器在什么情况下才会被触发,其次再看什么时候被调用的,最后我们在分析一下内置的处理器什么时候被使用的;

1. 触发条件

上一节中提到需要指定 void 返回类型,主要原因可以查看 MapperMethod 的 execute 方法,MapperMethod 在 Mybatis 之方法如何映射到 XML 中是有介绍的,重点讲到了 SqlCommand 和 MethodSignature 类,下面看一下 execute 方法部分代码:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
      ... 省略...
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);
          result = null;
        } 
      ... 省略...
    return result;
  }

可以看到什么情况下才会调用结果处理器有 三个要求
1. 首先必须是查询命令,其他的增删改是没有结果处理器的;
2. 其次是方法的返回值必须是 void,这其实也好理解,如果本身方法就有返回值,结果处理器也有返回值,反而容易搞混;
3. 必须要有结果处理器,这个肯定是必须的,没有也别谈结果处理器了。

2. 何时调用

具体何时调用我们自己的结果处理器,相关处理主要在 DefaultResultSetHandler 中,也就是在处理结果映射的时候,更多可以参考 Mybatis 之 XML 如何映射到方法,在主方法 handleResultSets 中调用的 handleResultSet 方法:

 private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {if (parentMapping != null) {handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {if (resultHandler == null) {DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {// issue #228 (close resultsets)
      closeResultSet(rsw.getResultSet());
    }
  }

重点看一下没有指定 parentMapping 的情况下,如果没有指定 resultHandler 则系统会创建一个默认的 DefaultResultHandler,如果有则用用户自己的处理器;然后就是通过对象工厂创建对象,然后通过类型处理器读取 ResultSet,最后调用 callResultHandler 方法来调用处理器:

  private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) {resultContext.nextResultObject(rowValue);
    ((ResultHandler<Object>) resultHandler).handleResult(resultContext);
  }

把封装好数据的对象 rowValue 放入 resultContext 中,然后传入处理器中进行处理;所以需要注意的是如果有多条记录其实是一条一条传入结果处理器进行处理的,并不是生成一个 list 然后交给 ResultHandler 处理,但是内置的 Map 结果集却不是这样处理的,接下来重点看一下 Mybatis 内置的两个处理器;

3. 内置结果处理器

3.1 DefaultResultHandler
内置了一个 ArrayList<Object> 对象,可以简单的理解为将结果集放入 list 中;但是上面我们也看到 DefaultResultHandler 并不是给用户使用的,而是 Mybatis 自己使用的,在用户没有指定处理器,Mybatis 会自己创建一个 DefaultResultHandler,而这个处理器是一个局部对象,每次 getResultList 之后其实还是放在了一个 multipleResults 中:

public List<Object> handleResultSets(Statement stmt) throws SQLException {final List<Object> multipleResults = new ArrayList<Object>();
    ... 处理 ResultSet...

这里定义的 multipleResults 是否可以替换成 DefaultResultHandler,感觉会更加合理;

3.2 DefaultMapResultHandler
内置了一个 HashMap 对象,此类也是被 Mybatis 内部使用在处理结果集是 Map 时使用,具体代码如下:

  public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowBounds) {final List<? extends V> list = selectList(statement, parameter, rowBounds);
    final DefaultMapResultHandler<K, V> mapResultHandler = new DefaultMapResultHandler<K, V>(mapKey,
        configuration.getObjectFactory(), configuration.getObjectWrapperFactory(), configuration.getReflectorFactory());
    final DefaultResultContext<V> context = new DefaultResultContext<V>();
    for (V o : list) {context.nextResultObject(o);
      mapResultHandler.handleResult(context);
    }
    return mapResultHandler.getMappedResults();}

这里首先获取结果集 list,然后遍历结果集通过 DefaultMapResultHandler 转成 Map 结果集;这里个人感觉可以直接把 DefaultMapResultHandler 作为参数传入 selectList 中,这样最后就无需再次遍历一遍;

自定义结果处理器

自定义一个结果处理器也很简单,实现 ResultHandler 接口即可,比如下面的 Map 处理器:

public class MyResultHandler implements ResultHandler<Blog> {Map<Long, Blog> result = new HashMap<Long, Blog>();

    @Override
    public void handleResult(ResultContext<? extends Blog> resultContext) {Blog blog = resultContext.getResultObject();
        System.out.println(blog.toString());
        result.put(blog.getId(), blog);
    }

    public Map<Long, Blog> getResult() {return result;}

}

简单测试一下,同样查询 Blog,如下所示:

    public static void selectMyHandler(SqlSession session) {BlogMapper mapper = session.getMapper(BlogMapper.class);
        MyResultHandler handler = new MyResultHandler();
        mapper.selectBlogsByHandler("zhaohui", handler);
        System.out.println(handler.getResult());
    }

日志输出如下:

Blog [id=159, title=hello java, author=zhaohui, content=hello java666]
Blog [id=160, title=hello java, author=zhaohui, content=hello java666]
{160=Blog [id=160, title=hello java, author=zhaohui, content=hello java666], 159=Blog [id=159, title=hello java, author=zhaohui, content=hello java666]}

可以发现分别打印了两次 Blog,因为每次生成一个 Blog 对象都会调用一次 handleResult;

总结

本文首先介绍了如何使用结果处理器,然后引出什么情况下才能触发处理器需要有三个条件,以及 Mybatis 内置的两个处理器分别处理 list 和 map,最后自定义了一个简单的结果处理器。

示例代码地址

Github

退出移动版