文章目录
- 一、目标:ResultMap映射参数
- 二、设计:ResultMap映射参数
- 三、实现:ResultMap映射参数
- 3.1 工程结构
- 3.2 ResultMap映射参数类图
- 3.3 添加类型处理器
- 3.3.1 日期类型处理器
- 3.3.2 类型处理器注册机
- 3.4 存放映射对象
- 3.4.1 结果标志
- 3.4.2 结果映射Map
- 3.4.3 封装结果映射
- 3.4.4 解析结果映射
- 3.5 解析映射配置
- 3.5.1 构建器基类
- 3.5.2 映射构建器助手
- 3.5.3 解析映射配置
- 3.6 使用映射对象
- 四、测试:ResultMap映射参数
- 4.1 测试环境配置
- 4.1.1 在mybatis数据库添加activity数据表
- 4.1.2 提供DAO接口和实体类
- 4.1.3 配置数据源和配置Mapper
- 4.1.4 活动接口配置文件
- 4.2 单元测试
- 五、总结:ResultMap映射参数
一、目标:ResultMap映射参数
💡 如何将数据库表中的下划线的字段名称,映射成Java代码中的驼峰字段?
- 通常在数据库表字段的命名中,所定义的规范是希望使用小写的英文字母和下划线的方式组合使用。
- 例如:雇员表中的雇员姓名,则使用
employee_name
表字段描述。
- 例如:雇员表中的雇员姓名,则使用
- 但这样的字段定义与 Java 代码开发中的 PO 数据库对象中的字段,是不能一一匹配的。因为 Java 代码中会使用驼峰的方式进行命名。
- 同样是雇员姓名在 Java 代码中则是
employeeName
的方式进行表示。
- 同样是雇员姓名在 Java 代码中则是
- 在使用 Mybatis 框架的时候,如果遇到这样的字段,则需要通过把数据库表中的下划线的字段名称,映射成 Java 代码中的驼峰字段,这样才能在执行查询操作的时候,正确的把数据库中的结果映射到 Java 代码的返回对象上。
注意
:在 Mybatis 中也可以使用例如employee_name as employeeName
的方式进行处理,但在整个编程中并不是太优雅。- 因为所有的查询都要做
as
映射,那么使用一个统一的字段映射更加合理。
- 因为所有的查询都要做
二、设计:ResultMap映射参数
💡 用一个标准的结构适配非映射类的对象属性。
- 之前处理解析 Mapper XML 中的
select
语句下配置的resultType
时,其实就已经添加了ResultMap、ResultMapping
的映射结构。 - 不过之前对于返回类型的处理都直接是对象类型,没有使用映射参数。而这也就代表着,在处理查询结果集后,SQL 对应的查询字段与 Java 代码中类的属性字段是一一对应的,所以不要使用映射,直接按照匹配的属性名称设置值即可。
- 之所以采用这样通用的结果类型包装结构,是为了做统一的方式处理,也相当于是在定义标准,用一个标准的结构适配非映射类的对象属性。
- 可以借助开发好的 ResultMap 封装参数结构,完善对字段映射的处理。
- 完善 ResultMapping 结果映射中 Builder 构建者的对映射属性的保存操作,并使用到解析 Mapper XML 中对 resultMap 元素的处理。
- 映射参数的解析过程:主要以循环解析 resultMap 的标签集合,摘取核心的
property、column
字段构建出 ResultMapping 结果映射类。- 每一条配置都会创建出一个 ResultMapping 类。
- 最后这个配置信息会被写入到 Configuration 配置项的
Map<String, ResultMap> resultMaps
的结果映射中。
- 最后在程序执行获取到 Mapper 一直调用到 DefaultSqlSession 查询结果封装时,再从配置项中把相关的 ResultMap 读取出来,进行设置属性值。
三、实现:ResultMap映射参数
3.1 工程结构
mybatis-step-13
|-src
|-main
| |-java
| |-com.lino.mybatis
| |-annotations
| | |-Delete.java
| | |-Insert.java
| | |-Select.java
| | |-Update.java
| |-binding
| | |-MapperMethod.java
| | |-MapperProxy.java
| | |-MapperProxyFactory.java
| | |-MapperRegistry.java
| |-builder
| | |-annotations
| | | |-MapperAnnotationBuilder.java
| | |-xml
| | | |-XMLConfigBuilder.java
| | | |-XMLMapperBuilder.java
| | | |-XMLStatementBuilder.java
| | |-BaseBuilder.java
| | |-MapperBuilderAssistant.java
| | |-ParameterExpression.java
| | |-ResultMapResolver.java
| | |-SqlSourceBuilder.java
| | |-StaticSqlSource.java
| |-datasource
| | |-druid
| | | |-DruidDataSourceFacroty.java
| | |-pooled
| | | |-PooledConnection.java
| | | |-PooledDataSource.java
| | | |-PooledDataSourceFacroty.java
| | | |-PoolState.java
| | |-unpooled
| | | |-UnpooledDataSource.java
| | | |-UnpooledDataSourceFacroty.java
| | |-DataSourceFactory.java
| |-executor
| | |-parameter
| | | |-ParameterHandler.java
| | |-result
| | | |-DefaultResultContext.java
| | | |-DefaultResultHandler.java
| | |-resultset
| | | |-DefaultResultSetHandler.java
| | | |-ResultSetHandler.java
| | | |-ResultSetWrapper.java
| | |-statement
| | | |-BaseStatementHandler.java
| | | |-PreparedStatementHandler.java
| | | |-SimpleStatementHandler.java
| | | |-StatementHandler.java
| | |-BaseExecutor.java
| | |-Executor.java
| | |-SimpleExecutor.java
| |-io
| | |-Resources.java
| |-mapping
| | |-BoundSql.java
| | |-Environment.java
| | |-MappedStatement.java
| | |-ParameterMapping.java
| | |-ResultFlag.java
| | |-ResultMap.java
| | |-ResultMapping.java
| | |-SqlCommandType.java
| | |-SqlSource.java
| |-parsing
| | |-GenericTokenParser.java
| | |-TokenHandler.java
| |-reflection
| | |-factory
| | | |-DefaultObjectFactory.java
| | | |-ObjectFactory.java
| | |-invoker
| | | |-GetFieldInvoker.java
| | | |-Invoker.java
| | | |-MethodInvoker.java
| | | |-SetFieldInvoker.java
| | |-property
| | | |-PropertyNamer.java
| | | |-PropertyTokenizer.java
| | |-wrapper
| | | |-BaseWrapper.java
| | | |-BeanWrapper.java
| | | |-CollectionWrapper.java
| | | |-DefaultObjectWrapperFactory.java
| | | |-MapWrapper.java
| | | |-ObjectWrapper.java
| | | |-ObjectWrapperFactory.java
| | |-MetaClass.java
| | |-MetaObject.java
| | |-Reflector.java
| | |-SystemMetaObject.java
| |-scripting
| | |-defaults
| | | |-DefaultParameterHandler.java
| | | |-RawSqlSource.java
| | |-xmltags
| | | |-DynamicContext.java
| | | |-MixedSqlNode.java
| | | |-SqlNode.java
| | | |-StaticTextSqlNode.java
| | | |-XMLLanguageDriver.java
| | | |-XMLScriptBuilder.java
| | |-LanguageDriver.java
| | |-LanguageDriverRegistry.java
| |-session
| | |-defaults
| | | |-DefaultSqlSession.java
| | | |-DefaultSqlSessionFactory.java
| | |-Configuration.java
| | |-ResultContext.java
| | |-ResultHandler.java
| | |-RowBounds.java
| | |-SqlSession.java
| | |-SqlSessionFactory.java
| | |-SqlSessionFactoryBuilder.java
| | |-TransactionIsolationLevel.java
| |-transaction
| | |-jdbc
| | | |-JdbcTransaction.java
| | | |-JdbcTransactionFactory.java
| | |-Transaction.java
| | |-TransactionFactory.java
| |-type
| | |-BaseTypeHandler.java
| | |-DateTypeHandler.java
| | |-IntegerTypeHandler.java
| | |-JdbcType.java
| | |-LongTypeHandler.java
| | |-StringTypeHandler.java
| | |-TypeAliasRegistry.java
| | |-TypeHandler.java
| | |-TypeHandlerRegistry.java
|-test
|-java
| |-com.lino.mybatis.test
| |-dao
| | |-IActivityDao.java
| |-po
| | |-Activity.java
| |-ApiTest.java
|-resources
|-mapper
| |-Activity_Mapper.xml
|-mybatis-config-datasource.xml
3.2 ResultMap映射参数类图
- 以 XMLMapperBuilder 解析为入口,扩展
resultMapElements
方法,解析resultMap
映射参数。- 解析过程涉及到 MapperBuilderAssistant 映射器构建助手类的使用,所以需要在 XMLMapperBuilder 构建函数中进行初始化。
- 参数的解析细节主要在 MapperBuilderAssistant 映射构建器助手中完成。
- 包括解析
javaTypeClass、typeHandlerInstance
,以及封装 XML 配置的基本字段映射信息。
- 包括解析
- 一个 DAO 方法的执行,需要从 Mapper 映射器获取开始,并逐步拿到映射器代理、映射器方法和 DefaultSqlSession 的执行。
- 最终在执行后封装返回结果时,就可以按照我们已经提供好的结果映射参数进行处理。
- 这部分操作也就是
DefaultResultSetHandler#applyPropertyMappings
的处理过程。
- 这部分操作也就是
3.3 添加类型处理器
3.3.1 日期类型处理器
DateTypeHandler.java
package com.lino.mybatis.type;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
/**
* @description: 日期类型处理器
* @author: lingjian
* @createDate: 2022/11/11 14:01
*/
public class DateTypeHandler extends BaseTypeHandler<Date> {
@Override
protected void setNonNullParameter(PreparedStatement ps, int i, Date parameter, JdbcType jdbcType) throws SQLException {
ps.setTimestamp(i, new Timestamp((parameter).getTime()));
}
@Override
protected Date getNullableResult(ResultSet rs, String columnName) throws SQLException {
Timestamp sqlTimestamp = rs.getTimestamp(columnName);
if (sqlTimestamp != null) {
return new Date(sqlTimestamp.getTime());
}
return null;
}
}
3.3.2 类型处理器注册机
TypeHandlerRegistry.java
package com.lino.mybatis.type;
import java.lang.reflect.Type;
import java.util.Date;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 类型处理器注册机
*/
public final class TypeHandlerRegistry {
private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<>(16);
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLER_MAP = new HashMap<>(16);
public TypeHandlerRegistry() {
register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(String.class, new StringTypeHandler());
register(String.class, JdbcType.CHAR, new StringTypeHandler());
register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
register(Date.class, new DateTypeHandler());
}
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
register(javaType, null, typeHandler);
}
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
if (null != javaType) {
Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.computeIfAbsent(javaType, k -> new HashMap<>(16));
map.put(jdbcType, handler);
}
ALL_TYPE_HANDLER_MAP.put(handler.getClass(), handler);
}
@SuppressWarnings("unchecked")
public TypeHandler<?> getTypeHandler(Class<?> type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}
public boolean hasTypeHandler(Class<?> javaType) {
return hasTypeHandler(javaType, null);
}
public boolean hasTypeHandler(Class<?> javaType, JdbcType jdbcType) {
return javaType != null && getTypeHandler((Type) javaType, jdbcType) != null;
}
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = TYPE_HANDLER_MAP.get(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
handler = jdbcHandlerMap.get(null);
}
}
// type driver generics here
return (TypeHandler<T>) handler;
}
public TypeHandler<?> getMappingTypeHandler(Class<? extends TypeHandler<?>> handlerType) {
return ALL_TYPE_HANDLER_MAP.get(handlerType);
}
}
3.4 存放映射对象
3.4.1 结果标志
ResultFlag.java
package com.lino.mybatis.mapping;
/**
* @description: 结果标志
*/
public enum ResultFlag {
/**
* 结果标志
*/
ID, CONSTRUCTOR
}
3.4.2 结果映射Map
ResultMapping.java
package com.lino.mybatis.mapping;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.type.JdbcType;
import com.lino.mybatis.type.TypeHandler;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 结果映射Map
*/
public class ResultMapping {
private Configuration configuration;
private String property;
private String column;
private Class<?> javaType;
private TypeHandler<?> typeHandler;
private List<ResultFlag> flags;
public ResultMapping() {
}
public static class Builder {
private ResultMapping resultMapping = new ResultMapping();
public Builder(Configuration configuration, String property, String column, Class<?> javaType) {
resultMapping.configuration = configuration;
resultMapping.property = property;
resultMapping.column = column;
resultMapping.javaType = javaType;
resultMapping.flags = new ArrayList<>();
}
public Builder typeHandler(TypeHandler<?> typeHandler) {
resultMapping.typeHandler = typeHandler;
return this;
}
public Builder flags(List<ResultFlag> flags) {
resultMapping.flags = flags;
return this;
}
public ResultMapping build() {
resolveTypeHandler();
return resultMapping;
}
private void resolveTypeHandler() {
if (resultMapping.typeHandler == null && resultMapping.javaType != null) {
Configuration configuration = resultMapping.configuration;
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, null);
}
}
}
public Configuration getConfiguration() {
return configuration;
}
public String getProperty() {
return property;
}
public String getColumn() {
return column;
}
public Class<?> getJavaType() {
return javaType;
}
public TypeHandler<?> getTypeHandler() {
return typeHandler;
}
public List<ResultFlag> getFlags() {
return flags;
}
}
3.4.3 封装结果映射
- ResultMap 映射对象的封装主要包括了对象的构建和结果的存放。
- 存放的地点就是 Configuration 配置项中所提供的结果映射
Map<String, ResultMap> resultMaps
。 - 这样的配置方式也是为了后续可以通过
resultMaps Key
获取到对应的 ResultMap 进行使用。
ResultMap.java
package com.lino.mybatis.mapping;
import com.lino.mybatis.session.Configuration;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* @description: 结果映射
*/
public class ResultMap {
private String id;
private Class<?> type;
private List<ResultMapping> resultMappings;
private Set<String> mappedColumns;
public ResultMap() {
}
public static class Builder {
private ResultMap resultMap = new ResultMap();
public Builder(Configuration configuration, String id, Class<?> type, List<ResultMapping> resultMappings) {
resultMap.id = id;
resultMap.type = type;
resultMap.resultMappings = resultMappings;
}
public ResultMap build() {
resultMap.mappedColumns = new HashSet<>();
// 添加 mappedColums 字段
for (ResultMapping resultMapping : resultMap.resultMappings) {
final String column = resultMapping.getColumn();
if (column != null) {
resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));
}
}
return resultMap;
}
}
public String getId() {
return id;
}
public Class<?> getType() {
return type;
}
public List<ResultMapping> getResultMappings() {
return resultMappings;
}
public Set<String> getMappedColumns() {
return mappedColumns;
}
public List<ResultMapping> getPropertyResultMappings() {
return resultMappings;
}
}
- ResultMap 中 Builder 建造者负责完成字段的处理,通过把字段统一转换为大写存放到
mappedColumns
映射字段中。并返回resultMap
对象。 - 其余的信息都可以通过构造函数进行传递。
3.4.4 解析结果映射
ResultMapResolver.java
package com.lino.mybatis.builder;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.mapping.ResultMapping;
import java.util.List;
/**
* @description: 结果映射解析器
*/
public class ResultMapResolver {
private final MapperBuilderAssistant assistant;
private String id;
private Class<?> type;
private List<ResultMapping> resultMappings;
public ResultMapResolver(MapperBuilderAssistant assistant, String id, Class<?> type, List<ResultMapping> resultMappings) {
this.assistant = assistant;
this.id = id;
this.type = type;
this.resultMappings = resultMappings;
}
public ResultMap resolve() {
return assistant.addResultMap(this.id, this.type, this.resultMappings);
}
}
- 新增 ResultMapResolver 结果映射器,它的作用就是对解析结果内容的一个封装处理。
- 最终调用的还是 MapperBuilderAssistant 映射构建器助手,所提供 ResultMap 封装和保存操作。
3.5 解析映射配置
- 整个配置解析都以围绕 Mybatis 框架中使用
resultMap
映射为主。 - 而
resultMap
的参数映射配置也是用于解决数据库表中的字段与 Java 代码中的对象字段不一致的情况。
3.5.1 构建器基类
BaseBuilder.java
package com.lino.mybatis.builder;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.type.TypeAliasRegistry;
import com.lino.mybatis.type.TypeHandler;
import com.lino.mybatis.type.TypeHandlerRegistry;
/**
* @description: 构建器的基类,建造者模式
*/
public class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
public Configuration getConfiguration() {
return configuration;
}
protected Class<?> resolveAlias(String alias) {
return typeAliasRegistry.resolveAlias(alias);
}
/**
* 根据别名解析 Class 类型别名注册/事务管理器别名
*
* @param alias 别名
* @return 对象类型
*/
protected Class<?> resolveClass(String alias) {
if (alias == null) {
return null;
}
try {
return resolveAlias(alias);
} catch (Exception e) {
throw new RuntimeException("Error resolving class. Cause: " + e, e);
}
}
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
if (typeHandlerType == null) {
return null;
}
return typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
}
}
3.5.2 映射构建器助手
MapperBuilderAssistant.java
package com.lino.mybatis.builder;
import com.lino.mybatis.mapping.*;
import com.lino.mybatis.reflection.MetaClass;
import com.lino.mybatis.scripting.LanguageDriver;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.type.TypeHandler;
import java.util.ArrayList;
import java.util.List;
/**
* @description: 映射构建器助手,建造者
*/
public class MapperBuilderAssistant extends BaseBuilder {
private String currentNamespace;
private String resource;
public MapperBuilderAssistant(Configuration configuration, String resource) {
super(configuration);
this.resource = resource;
}
public ResultMapping buildResultMapping(Class<?> resultType, String property, String column, List<ResultFlag> flags) {
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, null);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, null);
ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property, column, javaTypeClass);
builder.typeHandler(typeHandlerInstance);
builder.flags(flags);
return builder.build();
}
private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
if (javaType == null && property != null) {
try {
MetaClass metaResultType = MetaClass.forClass(resultType);
javaType = metaResultType.getSetterType(property);
} catch (Exception ignore) {
}
}
if (javaType == null) {
javaType = Object.class;
}
return javaType;
}
public String getCurrentNamespace() {
return currentNamespace;
}
public void setCurrentNamespace(String currentNamespace) {
this.currentNamespace = currentNamespace;
}
public String applyCurrentNamespace(String base, boolean isReference) {
if (base == null) {
return null;
}
if (isReference) {
if (base.contains(".")) {
return base;
}
} else {
if (base.startsWith(currentNamespace + ".")) {
return base;
}
if (base.contains(".")) {
throw new RuntimeException("Dots are not allowed in element names, please remove it from " + base);
}
}
return currentNamespace + "." + base;
}
/**
* 添加映射器语句
*/
public MappedStatement addMappedStatement(String id, SqlSource sqlSource, SqlCommandType sqlCommandType,
Class<?> parameterType, String resultMap, Class<?> resultType,
LanguageDriver lang) {
// 给id加上namespace前缀:com.lino.mybatis.test.dao.IUserDao.queryUserInfoById
id = applyCurrentNamespace(id, false);
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlCommandType, sqlSource, resultType);
// 结果映射, 给 MappedStatement#resultMaps
setStatementResultMap(resultMap, resultType, statementBuilder);
MappedStatement statement = statementBuilder.build();
// 映射语句信息,建造完存放到配置项中
configuration.addMappedStatement(statement);
return statement;
}
private void setStatementResultMap(String resultMap, Class<?> resultType, MappedStatement.Builder statementBuilder) {
// 因为暂时还没有在 Mapper XML 中配置 Map 返回结果,所以这里返回的是 null
resultMap = applyCurrentNamespace(resultMap, true);
List<ResultMap> resultMaps = new ArrayList<>();
if (resultMap != null) {
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
resultMaps.add(configuration.getResultMap(resultMapName.trim()));
}
}
/*
* 通常使用 resultType 即可满足大部分场景
* <select id="queryUserInfoById" resultType="com.lino.mybatis.test.po.User">
* 使用 resultType 的情况下,Mybatis 会自动创建一个 ResultMap,基于属性名称映射列到 JavaBean 的属性上。
*/
else if (resultType != null) {
ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(
configuration, statementBuilder.id() + "-Inline", resultType, new ArrayList<>());
resultMaps.add(inlineResultMapBuilder.build());
}
statementBuilder.resultMaps(resultMaps);
}
public ResultMap addResultMap(String id, Class<?> type, List<ResultMapping> resultMappings) {
// 补全ID全路径,如:com.lino.mybatis.test.dao.IActivityDao + activityMap
id = applyCurrentNamespace(id, false);
ResultMap.Builder inlineResultMapBuilder = new ResultMap.Builder(configuration, id, type, resultMappings);
ResultMap resultMap = inlineResultMapBuilder.build();
configuration.addResultMap(resultMap);
return resultMap;
}
}
- 在 MapperBuilderAssistant 映射构建器助手中,新增加了2个方法。
- 构建 Mapping 方法
buildResultMapping
。- 在最开始 Mapper XML 映射构建器解析
buildResultMappingFromContext
所调用的XMLMapperBuilder#buildResultMapping
方法。 - 封装映射配置中
<result column="activity_id" property="activityId" />
的column、property
字段。
- 在最开始 Mapper XML 映射构建器解析
- 添加 ResultMap 方法
addResultMap
。- 从 ResultMapResolver 结果映射器调用添加 ResultMap。
- 最终就是把这个配置保存到 Configuration 配置项中。
- 构建 Mapping 方法
3.5.3 解析映射配置
- 基于这样对映射字段的解决方案,所以需要扩展 Mapper XML 映射构建器
configurationElement
方法的处理内容。 - 添加解析
resultMap
操作。这部分操作也就是在解析整个select、insert、update、delete
部分。
XMLMapperBuilder.java
package com.lino.mybatis.builder.xml;
import com.lino.mybatis.builder.BaseBuilder;
import com.lino.mybatis.builder.MapperBuilderAssistant;
import com.lino.mybatis.builder.ResultMapResolver;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.mapping.ResultFlag;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.mapping.ResultMapping;
import com.lino.mybatis.session.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @description: XML映射构建器
*/
public class XMLMapperBuilder extends BaseBuilder {
private Element element;
private String resource;
/**
* 映射器构建助手
*/
private MapperBuilderAssistant builderAssistant;
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource) throws DocumentException {
this(new SAXReader().read(inputStream), configuration, resource);
}
public XMLMapperBuilder(Document document, Configuration configuration, String resource) {
super(configuration);
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.element = document.getRootElement();
this.resource = resource;
}
/**
* 解析
*
* @throws Exception 异常
*/
public void parse() throws Exception {
// 如果当前资源没有加载过再加载,防止重复加载
if (!configuration.isResourceLoaded(resource)) {
configurationElement(element);
// 标记一下,已经加载过了
configuration.addLoadedResource(resource);
// 绑定映射器到namespace
configuration.addMapper(Resources.classForName(builderAssistant.getCurrentNamespace()));
}
}
/**
* 配置mapper元素
* <mapper namespace="org.mybatis.example.BlogMapper">
* <select id="selectBlog" parameterType="int" resultType="Blog">
* select * from Blog where id = #{id}
* </select>
* </mapper>
*
* @param element 元素
*/
private void configurationElement(Element element) {
// 1.配置namespace
String namespace = element.attributeValue("namespace");
if ("".equals(namespace)) {
throw new RuntimeException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 2.解析resultMap
resultMapElement(element.elements("resultMap"));
// 3.配置select|insert|update|delete
buildStatementFromContext(element.elements("select"), element.elements("insert"), element.elements("update"), element.elements("delete"));
}
/**
* 解析resultMap
*
* @param list 结果映射列表
*/
private void resultMapElement(List<Element> list) {
for (Element element : list) {
try {
resultMapElement(element, Collections.emptyList());
} catch (Exception ignore) {
}
}
}
/**
* <resultMap id="activityMap" type="cn.bugstack.mybatis.test.po.Activity">
* <id column="id" property="id"/>
* <result column="activity_id" property="activityId"/>
* <result column="activity_name" property="activityName"/>
* <result column="activity_desc" property="activityDesc"/>
* <result column="create_time" property="createTime"/>
* <result column="update_time" property="updateTime"/>
* </resultMap>
*/
private ResultMap resultMapElement(Element resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
String id = resultMapNode.attributeValue("id");
String type = resultMapNode.attributeValue("type");
Class<?> typeClass = resolveClass(type);
List<ResultMapping> resultMappings = new ArrayList<>();
resultMappings.addAll(additionalResultMappings);
List<Element> resultChildren = resultMapNode.elements();
for (Element resultChild : resultChildren) {
List<ResultFlag> flags = new ArrayList<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// 构建 ResultMapping
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
// 创建结果映射解析器
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, resultMappings);
return resultMapResolver.resolve();
}
/**
* <id column="id" property="id"/>
* <result column="activity_id" property="activityId"/>
*/
private ResultMapping buildResultMappingFromContext(Element context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String property = context.attributeValue("property");
String column = context.attributeValue("column");
return builderAssistant.buildResultMapping(resultType, property, column, flags);
}
/**
* 配置select|insert|update|delete
*
* @param lists 元素列表
*/
@SafeVarargs
private final void buildStatementFromContext(List<Element>... lists) {
for (List<Element> list : lists) {
for (Element element : list) {
final XMLStatementBuilder statementBuilder = new XMLStatementBuilder(configuration, builderAssistant, element);
statementBuilder.parseStatementNode();
}
}
}
}
- 在
XMLMapperBuilder#configurationElement
配置元素解析的方法中,新增加了关于resultMap
元素的解析。- 由于可能在一个 Mapper XML 中有多组这样的映射参数配置,所以这里获取的是一个
elements
集合元素。
- 由于可能在一个 Mapper XML 中有多组这样的映射参数配置,所以这里获取的是一个
- 解析的核心过程包括:
- 读取
resultMap
标签中,如<resultMap id="activityMap" type="com.lino.mybatis.test.po.Activity">
的id、type
信息。 - 之后循环解析标签内的每条配置元素,如
<result column="activity_id" property="activityId"/>
中的column、property
信息。 - 同时会把
id
的配置专门用 ResultFlag 枚举类进行标记。 - 基础信息解析完成后,就开始调用结果映射器把解析的信息封装成 ResultMap 进行存放。
- 读取
3.6 使用映射对象
- 从 DefaultSqlSession 调用方法,执行 SQL 后,就是对结果的封装。
- 主要体现在
DefaultResultSetHandler#handlerResultSets
结果收集器的操作中。
- 主要体现在
DefaultResultSetHandler.java
package com.lino.mybatis.executor.resultset;
import com.lino.mybatis.executor.Executor;
import com.lino.mybatis.executor.result.DefaultResultContext;
import com.lino.mybatis.executor.result.DefaultResultHandler;
import com.lino.mybatis.mapping.BoundSql;
import com.lino.mybatis.mapping.MappedStatement;
import com.lino.mybatis.mapping.ResultMap;
import com.lino.mybatis.mapping.ResultMapping;
import com.lino.mybatis.reflection.MetaClass;
import com.lino.mybatis.reflection.MetaObject;
import com.lino.mybatis.reflection.factory.ObjectFactory;
import com.lino.mybatis.session.Configuration;
import com.lino.mybatis.session.ResultHandler;
import com.lino.mybatis.session.RowBounds;
import com.lino.mybatis.type.TypeHandler;
import com.lino.mybatis.type.TypeHandlerRegistry;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* @description: 默认Map结果处理器
* @author: lingjian
* @createDate: 2022/11/8 13:59
*/
public class DefaultResultSetHandler implements ResultSetHandler {
private static final Object NO_VALUE = new Object();
...
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
// 根据返回类型,实例化对象
Object resultObject = createResultObject(rsw, resultMap, null);
if (resultObject != null && !typeHandlerRegistry.hasTypeHandler(resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(resultObject);
// 自动映射:把每列的值都赋到对应的字段上
applyAutomaticMappings(rsw, resultMap, metaObject, null);
// Map映射:根据映射类型赋值到字段
applyPropertyMappings(rsw, resultMap, metaObject, null);
}
return resultObject;
}
...
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
final String column = propertyMapping.getColumn();
if (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH))) {
// 获取值
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
Object value = typeHandler.getResult(rsw.getResultSet(), column);
// 设置值
final String property = propertyMapping.getProperty();
if (value != NO_VALUE && property != null && value != null) {
// 通过反射工具类设置属性值
metaObject.setValue(property, value);
foundValues = true;
}
}
}
return foundValues;
}
}
- 从
DefaultResultSetHandler#handlerResultSets
方法开始,调用handlerResultSet
方法,创建结果处理器、封装数据和保存结果。 - 那么从封装数据阶段,则包括了创建对象和封装对象属性。
- 在
DefaultResultSetHandler#getRowValue
方法中,原有的是通过自动映射,把每列的值赋值到对应的字段上。 - 而现在因为有了属性映射,所以需要新添加
applyPropertyMappings
方法进行处理。- 在
applyPropertyMappings
首先获取mappedColumnNames
映射的字段。 - 在后续循环处理
List<ResultMapping>
时,进行比对判断是否包含当前字段。 - 如果包含则获取到对应类型的 TypeHandler 类型处理器,执行
TypeHandler#getResult
获取结果。 - 并把这个结果通过 MetaObject 反射工具类把结果设置到对象对应的属性中。
- 在
四、测试:ResultMap映射参数
4.1 测试环境配置
4.1.1 在mybatis数据库添加activity数据表
CREATE TABLE `activity` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`activity_id` bigint(20) NOT NULL COMMENT '活动ID',
`activity_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '活动名称',
`activity_desc` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '活动描述',
`create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unique_activity_id`(`activity_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '活动配置' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of activity
-- ----------------------------
INSERT INTO `activity` VALUES (1, 100001, '活动名', '测试活动', '2021-08-08 20:14:50', '2021-08-08 20:14:50');
INSERT INTO `activity` VALUES (3, 100002, '活动名', '测试活动', '2021-10-05 15:49:21', '2021-10-05 15:49:21');
4.1.2 提供DAO接口和实体类
Activity.java
package com.lino.mybatis.test.po;
import java.util.Date;
/**
* @description: 活动类
*/
public class Activity {
/**
* 主键ID
*/
private Long id;
/**
* 活动ID
*/
private Long activityId;
/**
* 活动名称
*/
private String activityName;
/**
* 活动描述
*/
private String activityDesc;
/**
* 创建人
*/
private String creator;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Long getActivityId() {
return activityId;
}
public void setActivityId(Long activityId) {
this.activityId = activityId;
}
public String getActivityName() {
return activityName;
}
public void setActivityName(String activityName) {
this.activityName = activityName;
}
public String getActivityDesc() {
return activityDesc;
}
public void setActivityDesc(String activityDesc) {
this.activityDesc = activityDesc;
}
public String getCreator() {
return creator;
}
public void setCreator(String creator) {
this.creator = creator;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
}
IActivityDao.java
package com.lino.mybatis.test.dao;
import com.lino.mybatis.test.po.Activity;
/**
* @description: 活动持久层
*/
public interface IActivityDao {
/**
* 根据活动ID查询活动
*
* @param activityId 活动ID
* @return 活动对象
*/
Activity queryActivityById(Long activityId);
}
4.1.3 配置数据源和配置Mapper
mybatis-config-datasource.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url"
value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>
<mappers>
<!--XML配置-->
<mapper resource="mapper/Activity_Mapper.xml"/>
</mappers>
</configuration>
4.1.4 活动接口配置文件
Activity_Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lino.mybatis.test.dao.IActivityDao">
<resultMap id="activityMap" type="com.lino.mybatis.test.po.Activity">
<id column="id" property="id"/>
<result column="activity_id" property="activityId"/>
<result column="activity_name" property="activityName"/>
<result column="activity_desc" property="activityDesc"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
<select id="queryActivityById" parameterType="java.lang.Long" resultMap="activityMap">
SELECT activity_id, activity_name, activity_desc, create_time, update_time
FROM activity
WHERE activity_id = #{activityId}
</select>
</mapper>
4.2 单元测试
ApiTest.java
package com.lino.mybatis.test;
import com.alibaba.fastjson.JSON;
import com.lino.mybatis.io.Resources;
import com.lino.mybatis.session.SqlSession;
import com.lino.mybatis.session.SqlSessionFactory;
import com.lino.mybatis.session.SqlSessionFactoryBuilder;
import com.lino.mybatis.test.dao.IActivityDao;
import com.lino.mybatis.test.po.Activity;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
/**
* @description: 单元测试
*/
public class ApiTest {
private Logger logger = LoggerFactory.getLogger(ApiTest.class);
private SqlSession sqlSession;
@Before
public void init() throws IOException {
// 1.从SqlSessionFactory中获取SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader("mybatis-config-datasource.xml"));
sqlSession = sqlSessionFactory.openSession();
}
@Test
public void test_queryActivityById() {
// 1.获取映射器对象
IActivityDao dao = sqlSession.getMapper(IActivityDao.class);
// 2.测试验证
Activity result = dao.queryActivityById(100001L);
logger.info("测试结果:{}", JSON.toJSONString(result));
}
}
测试结果
16:37:14.750 [main] INFO c.l.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:activityId propertyType:class java.lang.Long
16:37:14.794 [main] INFO c.l.m.s.defaults.DefaultSqlSession - 执行查询 statement:com.lino.mybatis.test.dao.IActivityDao.queryActivityById parameter:100001
16:37:15.518 [main] INFO c.l.m.d.pooled.PooledDataSource - Created connention 1516500233.
16:37:15.526 [main] INFO c.l.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:100001
16:37:15.553 [main] INFO com.lino.mybatis.test.ApiTest - 测试结果:{"activityDesc":"测试活动","activityId":100001,"activityName":"活动名","createTime":1628424890000,"updateTime":1628424890000}
- 从 Debug 调试截图中的字段映射匹配和值的填充,测试结果看是通过的。
五、总结:ResultMap映射参数
- 本章结合整个框架和 ResultMap 提前预留出来的参数解析框架,添加映射类参数的处理操作。
- 在整个解析的过程中,一个 ResultMap 对应多个 ResultMapping 的关系,把每一条映射都处理成 ResultMapping 信息,都存放到配置项中。
- 前面提到过 Configuration 伴随着整个 session 生命周期。
- 所有的解析操作完成以后就是到了接触处理封装中。
- 思考:怎么把 SQL 执行的结果和对象封装到一起,普通的对象默认按照对象字段即可封装,而带有下划线的属性字段,则需要根据映射的2个字段,下划线对应非下划线的方式,进行匹配处理,最终返回统一的封装对象结果。