问题描述
在使用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;
}
}