美文网首页框架高效开发工具集
Mybatis注解支持默认Results

Mybatis注解支持默认Results

作者: 无醉_1866 | 来源:发表于2019-10-05 13:10 被阅读0次

问题描述

在使用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;
  }

}

相关文章

网友评论

    本文标题:Mybatis注解支持默认Results

    本文链接:https://www.haomeiwen.com/subject/fbwepctx.html