前言
学习源码是在学习什么呢?
就是为了通过这些源码级复杂模型中,学习系统框架的架构思维、设计原则和设计模式。在这些源码学习手写的过程中,感受、吸收并也是锻炼一种思维习惯,并尝试把这些思路技术迁移到平常的复杂业务设计开发中。
目标
通常在数据库表字段的命名中,所定义的规范是希望使用小写的英文字母和下划线的方式组合使用,例如:雇员表中的雇员姓名,则使用 employee_name 表字段描述。但这样的字段定义与 Java 代码开发中的 PO 数据库对象中的字段,是不能一一匹配的,因为 Java 代码中会使用驼峰的方式进行命名,同样是雇员姓名在 Java 代码中则是 employeeName 的方式进行表示。
那么这里就有一个问题了,我们在使用 Mybatis 框架的时候,如果遇到这样的字段,则需要通过把数据库表中的下划线的字段名称,映射成 Java 代码中的驼峰字段,这样才能在执行查询操作的时候,正确的把数据库中的结果映射到 Java 代码的返回对象上。注意:另外在 Mybatis 中也可以使用例如 employee_name as employeeName 的方式进行处理,但在整个编程中并不是太优雅,因为所有的查询都要做 as 映射,那么使用一个统一的字段映射更加合理。
设计
以 XMLMapperBuilder 解析为入口,扩展 resultMapElements 方法,解析 resultMap 映射参数。解析过程涉及到了 MapperBuilderAssistant 映射器构建助手类的使用,所以需要在 XMLMapperBuilder 构造函数中进行初始化。
参数的解析细节主要在 MapperBuilderAssistant 映射构建器助手中完成,包括解析 javaTypeClass、typeHandlerInstance,以及封装 XML 配置的基本字段映射信息。
解析映射配置
整个配置解析都以围绕在 Mybatis ORM 框架中使用 resultMap 映射为主,而 resultMap 的参数映射配置也是用于解决数据库表中的字段与 Java 代码中的对象字段不一致的情况。
解析入口
基于这样对映射字段的解决方案,所以需要扩展 Mapper XML 映射构建器 configurationElement 方法的处理内容,添加解析 resultMap 操作。这部分的操作也就是在解析整个 select、insert、update、delete 之前就操作这个部分,因为后续解析 select 标签时,如果遇到有 resultMap 的配置参数,则可以直接调用已经解析的 resultMap 参数进行关联。
private void configurationElement(Element element) {// 1.配置namespaceString namespace = element.attributeValue("namespace");if (namespace.equals("")) {throw new RuntimeException("Mapper's namespace cannot be empty");}builderAssistant.setCurrentNamespace(namespace);// 2. 解析resultMapresultMapElements(element.elements("resultMap"));// 3.配置select|insert|update|deletebuildStatementFromContext(element.elements("select"),element.elements("insert"),element.elements("update"),element.elements("delete"));
}
在 XMLMapperBuilder#configurationElement 配置元素解析的方法中,新增加了关于 resultMap 元素的解析,由于可能在一个 Mapper XML 中有多组这样的映射参数配置,所以这里获取的是一个 elements 集合元素。
解析过程
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);}// 构建 ResultMappingresultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}// 创建结果映射解析器ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, resultMappings);return resultMapResolver.resolve();
}
解析的核心过程包括读取 resultMap 标签中,如: 的 id、type 信息。之后开始循环解析标签内的每一条配置元素 如 中的 colum、property 信息。同时这里会把id的配置专门用 ResultFlag 枚举类进行标记。
基础信息解析完成后,就开始调用结果映射器把解析的信息封装成 ResultMap 进行存放。
存放映射对象
public class ResultMapResolver {private final MapperBuilderAssistant assistant;private String id;private Class<?> type;private List<ResultMapping> resultMappings;// ... 省略构造函数public ResultMap resolve() {return assistant.addResultMap(this.id, this.type, this.resultMappings);}}
ResultMapResolver 结果映射器是本章节新增加的内容,其实它的作用就是对解析结果内容的一个封装处理,最终调用的是还是 MapperBuilderAssistant 映射构建器助手,所提供 ResultMap 封装和保存操作。
封装 ResultMap
public class ResultMap {private String id;private Class<?> type;private List<ResultMapping> resultMappings;private Set<String> mappedColumns;private 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<>();// 添加 mappedColumns 字段for (ResultMapping resultMapping : resultMap.resultMappings) {final String column = resultMapping.getColumn();if (column != null) {resultMap.mappedColumns.add(column.toUpperCase(Locale.ENGLISH));}}return resultMap;}}// ... 省略get方法}
ResultMap 中 Builder 建造者负责完成字段的处理,通过把字段统一转换为大写存放到 mappedColumns 映射字段中。并返回 resultMap 对象。其余的信息都可以通过构造函数进行传递。
添加 ResultMap
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();}public ResultMap addResultMap(String id, Class<?> type, List<ResultMapping> resultMappings) {// 补全ID全路径,如:cn.bugstack.mybatis.test.dao.IActivityDao + activityMapid = 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、添加 ResultMap 方法 addResultMap。
而构建 buildResultMapping 方法就是在最开始 Mapper XML 映射构建器解析 buildResultMappingFromContext 所调用的 XMLMapperBuilder#buildResultMapping 方法,封装映射配置中 column、property 字段。
另外一个是 MapperBuilderAssistant#addResultMap 方法,从 ResultMapResolver 结果映射器调用添加 ResultMap,最终就是把这个配置保存到 Configuration 配置项中。
使用映射对象
从 DefaultSqlSession 调用方法,执行 SQL 后,就是对结果的封装,主要体现在 DefaultResultSetHandler#handleResultSets 结果收集器的操作中。
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#handleResultSets 方法开始,调用 handleResultSet 方法,创建结果处理器、封装数据和保存结果。
那么在封装数据阶段,则包括了创建对象和封装对象属性,源码参考DefaultResultSetHandler#getRowValue
测试
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 DEFAULT NULL COMMENT '活动名称',activity_desc varchar(128) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '活动描述',create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',update_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',PRIMARY KEY (id ),UNIQUE KEY unique_activity_id (activity_id )
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin COMMENT='活动配置';
配置数据源
<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>
通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password
在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。
配置Mapper加载方式
<mappers><!-- XML 配置 --><mapper resource="mapper/Activity_Mapper.xml"/>
</mappers>
配置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="cn.bugstack.mybatis.test.dao.IActivityDao"><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><select id="queryActivityById" parameterType="java.lang.Long" resultMap="activityMap">SELECT activity_id, activity_name, activity_desc, create_time, update_timeFROM activityWHERE activity_id = #{activityId}</select></mapper>
映射对象类
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;// ... 省略 get/set}
单元测试
@Before
public void init() throws IOException {// 1. 从SqlSessionFactory中获取SqlSessionSqlSessionFactory 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 res = dao.queryActivityById(100001L);logger.info("测试结果:{}", JSON.toJSONString(res));
}
测试结果
总结
结合着整个框架和 ResultMap 提前预留出来的参数解析框架,添加映射类参数的处理操作。在整个解析的过程中,一个 ResultMap 对应多个 ResultMapping 的关系,把每一条映射都存处理成 ResultMapping 信息,都存放到配置项中。前面我们就提到过 Configuration 伴随整个 session 生命周期
所有的解析操作完成以后就是到了接触处理封装中,这里你可以思考怎么把 SQL 执行的结果和对象封装到一起,普通的对象默认按照对象字段即可封装,而带有下划线的属性字段,则需要根据映射的2个字段,下划线对应非下划线的方式,进行匹配处理。最终返回统一的封装对象结果。
好了到这里就结束了手写手写mybatis之解析和使用ResultMap映射参数配置的学习,大家一定要跟着动手操作起来。需要源码的 可si我获取;