Mybatis注解支持默认Results

问题描述

在使用mybatis的注解时,当需要做结果集的映射时,需要写@Results注解,例如:

  @Select("select id, name from user where id in (#{idList})")
  @Results({
      @Result(property = "id", column = "id"),
      @Result(property = "name", column = "name")
  })
  List<User> selectByIds(@Param("idList") List<Integer> idList);

当有多个查询时,有两种方式:

  • 第一种方式是继续使用注解,将@Results复制多份到不同的查询方法上:
  @Select("select id, name from user where id in (#{idList})")
  @Results({
      @Result(property = "id", column = "id"),
      @Result(property = "name", column = "name")
  })
  List<User> selectByIds(@Param("idList") List<Integer> idList);

@Select("select id, name from user where id = #{id}")
  @Results({
      @Result(property = "id", column = "id"),
      @Result(property = "name", column = "name")
  })
User selectByIds(@Param("id") int id);

这样做的问题在于同样的@Results被标记在了多个方法上,难以维护

  • 第二种方式是结合xml使用,在xml中配置<ResultMap>后,通过@ResultMap注解使用:
<resultMap id = "userResultMap" type="cn.yxffcode.mybatis.model.User">
  <id column="id" property="id"/>
  <result column="name" property="name"/>
</resultMap>
@Select("select id, name from user where id in (#{idList})")
@ResultMap("userResultMap")
List<User> selectByIds(@Param("idList") List<Integer> idList);

@Select("select id, name from user where id = #{id}")
@ResultMap("userResultMap")
User selectById(@Param("id") int id);

对于喜欢使用注解的同学来说,使用注解的目的就是为了减少工程中的xml,对于这种情况,如何减少mapper接口上的@Results,使得一个Mapper接口最少只需要写一个@Results呢?
最小的实现成本是使用mybatis插件来达到此目的。

效果

我们先展示一下最终的效果:

@DefaultResults(resultType = User.class, results = {
    @Result(property = "id", column = "id"),
    @Result(property = "name", column = "name")
})
public interface TestDao {

  /**
   * 使用默认ResultMap
   */
  @Select("select id, name from user where id in (#{idList})")
  List<User> selectByIds(@Param("idList") List<Integer> idList);

  /**
   * 使用默认ResultMap
   */
  @Select("select id, name from user where id = #{id}")
  User selectById(@Param("id") Integer id);

  /**
   * 不使用默认ResultMap
   */
  @Select("select id, name, other_column from user where id = #{id}")
  @Results({
      @Result(property = "id", column = "id"),
      @Result(property = "name", column = "name"),
      @Result(property = "otherProperty", column = "other_column")
  })
  User selectFull(@Param("id") int id);
}

实现方式

基本原理:根据MappedStatement中的ResultMap来判断是否使用默认的ResultMap,判断逻辑:

  • 如果没有DefaultResults则走原来的逻辑
  • 如果ResultMap中的type和DefaultResults的resultType相同,且ResultMapping为空,则使用DefaultResults,否则走原来的逻辑

先定义注解@DefaultResults:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface DefaultResults {
  Class<?> resultType();//结果类型
  Result[] results();//映射
}

再实现插件:


import com.google.common.collect.Maps;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Properties;
import java.util.concurrent.ConcurrentMap;

/**
 * 对可复用的{@link org.apache.ibatis.annotations.Results}的支持,如果没有标记{@link org.apache.ibatis.annotations.Results}
 * 或者{@link org.apache.ibatis.annotations.ResultMap}则使用默认的ResultMap
 * <p>
 * 需要将{@link DefaultResults}标记在映射接口上
 *
 * @author gaohang
 */
@Intercepts({
    @Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
            CacheKey.class, BoundSql.class})
})
public class DefaultResultMapInterceptor implements Interceptor {

  private static final Logger logger = LoggerFactory.getLogger(DefaultResultMapInterceptor.class);

  private ConcurrentMap<String, ResultMapHolder> defaultResultMaps = Maps.newConcurrentMap();

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    final MappedStatement ms = (MappedStatement) args[0];
    final List<ResultMap> resultMaps = ms.getResultMaps();

    final String statementId = ms.getId();
    final int i = statementId.lastIndexOf('.');
    if (i <= 0) {
      return invocation.proceed();
    }
    final String namespace = statementId.substring(0, i);

    final DefaultResults defaultResults = getDefaultResults(namespace);
    if (defaultResults == null) {
      return invocation.proceed();
    }

    if (shouldDoByCurrentResultMaps(resultMaps, defaultResults)) {
      return invocation.proceed();
    }
    //add a default ResultMap
    final ResultMap defaultResultMap = getDefaultResultMap(defaultResults, Class.forName(namespace), namespace, ms);
    if (defaultResultMap == null) {
      //没有默认的ResultMap
      return invocation.proceed();
    }
    //使用新的MappedStatement
    final MappedStatement mappedStatement = copyMappedStatement(ms, defaultResultMap);
    args[0] = mappedStatement;
    return invocation.proceed();
  }

  private boolean shouldDoByCurrentResultMaps(List<ResultMap> resultMaps, DefaultResults defaultResults) {
    if (CollectionUtils.isEmpty(resultMaps)) {
      return false;
    }
    for (int i = 0, j = resultMaps.size(); i < j; i++) {
      final ResultMap resultMap = resultMaps.get(i);
      if (CollectionUtils.isEmpty(resultMap.getMappedColumns()) && resultMap.getType() == defaultResults.resultType()) {
        return false;
      }
    }
    return true;
  }

  private ResultMap getDefaultResultMap(DefaultResults defaultResults, Class<?> mappingInterface, String namespace, MappedStatement ms) {
    final ResultMapHolder resultMapHolder = defaultResultMaps.get(namespace);
    if (resultMapHolder != null) {
      return resultMapHolder.resultMap;
    }
    final ResultMap resultMap = buildResultMap(namespace, ms, mappingInterface, defaultResults);
    defaultResultMaps.putIfAbsent(namespace, new ResultMapHolder(resultMap));
    return resultMap;
  }

  private ResultMap buildResultMap(String namespace, MappedStatement ms, Class<?> mappingInterface,
                                   DefaultResults defaultResults) {
    final MapperBuilderAssistant assistant = new MapperBuilderAssistant(ms.getConfiguration(), ms.getResource());
    final ResultMapBuilder resultMapBuilder = new ResultMapBuilder(assistant, mappingInterface);

    final List<ResultMapping> resultMappings = resultMapBuilder.applyResults(
        defaultResults.results(), defaultResults.resultType());

    final ResultMap.Builder builder = new ResultMap.Builder(ms.getConfiguration(),
        namespace + ".DefaultResultMap", defaultResults.resultType(), resultMappings);
    return builder.build();
  }

  public DefaultResults getDefaultResults(String namespace) {
    try {
      final Class<?> mappingInterface = Class.forName(namespace);
      return mappingInterface.getAnnotation(DefaultResults.class);
    } catch (ClassNotFoundException e) {
      logger.debug("load namespace class failed, maybe namespace {} is not a class", namespace, e);
      return null;
    }
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
  }

  private static final class ResultMapHolder {

    private static final ResultMapHolder NONE = new ResultMapHolder(null);

    private final ResultMap resultMap;

    private ResultMapHolder(ResultMap resultMap) {
      this.resultMap = resultMap;
    }
  }


  private MappedStatement copyMappedStatement(MappedStatement ms, ResultMap resultMap) {
    MappedStatement nms;
    nms = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), ms.getSqlSource(),
        ms.getSqlCommandType()).cache(ms.getCache()).databaseId(ms.getDatabaseId())
        .fetchSize(ms.getFetchSize()).flushCacheRequired(true).keyGenerator(ms.getKeyGenerator())
        .parameterMap(ms.getParameterMap()).resource(ms.getResource())
        .resultMaps(Collections.singletonList(resultMap)).resultSetType(ms.getResultSetType())
        .statementType(ms.getStatementType()).timeout(ms.getTimeout()).useCache(ms.isUseCache())
        .build();
    setField(nms, "keyColumns", ms.getKeyColumns());
    setField(nms, "keyProperties", ms.getKeyProperties());
    return nms;
  }

}


import com.google.common.collect.Lists;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.builder.BuilderException;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.mapping.FetchType;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.UnknownTypeHandler;

import java.util.ArrayList;
import java.util.List;

import static com.google.common.base.Strings.nullToEmpty;

/**
 * @author gaohang
 */
final class ResultMapBuilder {
  private final MapperBuilderAssistant assistant;
  private final Class<?> type;
  private final Configuration configuration;

  ResultMapBuilder(MapperBuilderAssistant assistant, Class<?> type) {
    this.assistant = assistant;
    this.type = type;
    this.configuration = assistant.getConfiguration();
  }


  public List<ResultMapping> applyResults(Result[] results, Class<?> resultType) {
    final List<ResultMapping> resultMappings = Lists.newArrayListWithCapacity(results.length);
    for (Result result : results) {
      List<ResultFlag> flags = new ArrayList<>();
      if (result.id()) {
        flags.add(ResultFlag.ID);
      }
      @SuppressWarnings("unchecked")
      Class<? extends TypeHandler<?>> typeHandler = (Class<? extends TypeHandler<?>>)
          ((result.typeHandler() == UnknownTypeHandler.class) ? null : result.typeHandler());
      ResultMapping resultMapping = assistant.buildResultMapping(
          resultType,
          nullToEmpty(result.property()),
          nullToEmpty(result.column()),
          result.javaType() == void.class ? null : result.javaType(),
          result.jdbcType() == JdbcType.UNDEFINED ? null : result.jdbcType(),
          hasNestedSelect(result) ? nestedSelectId(result) : null,
          null,
          null,
          null,
          typeHandler,
          flags,
          null,
          null,
          isLazy(result));
      resultMappings.add(resultMapping);
    }
    return resultMappings;
  }

  private boolean hasNestedSelect(Result result) {
    if (result.one().select().length() > 0 && result.many().select().length() > 0) {
      throw new BuilderException("Cannot use both @One and @Many annotations in the same @Result");
    }
    return result.one().select().length() > 0 || result.many().select().length() > 0;
  }

  private String nestedSelectId(Result result) {
    String nestedSelect = result.one().select();
    if (nestedSelect.length() < 1) {
      nestedSelect = result.many().select();
    }
    if (!nestedSelect.contains(".")) {
      nestedSelect = type.getName() + "." + nestedSelect;
    }
    return nestedSelect;
  }

  private boolean isLazy(Result result) {
    boolean isLazy = configuration.isLazyLoadingEnabled();
    if (result.one().select().length() > 0 && FetchType.DEFAULT != result.one().fetchType()) {
      isLazy = result.one().fetchType() == FetchType.LAZY;
    } else if (result.many().select().length() > 0 && FetchType.DEFAULT != result.many().fetchType()) {
      isLazy = result.many().fetchType() == FetchType.LAZY;
    }
    return isLazy;
  }

}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容