本系列文章是我从《通用源码指导书:MyBatis源码详解》一书中的笔记和总结
本书是基于MyBatis-3.5.2版本,书作者 易哥 链接里是CSDN中易哥的微博。但是翻看了所有文章里只有一篇简单的介绍这本书。并没有过多的展示该书的魅力。接下来我将自己的学习总结记录下来。如果作者认为我侵权请联系删除,再次感谢易哥提供学习素材。本段说明将伴随整个系列文章,尊重原创,本人已在微信读书购买改书。
版权声明:本文为CSDN博主「架构师易哥」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/onlinedct/article/details/107306041
1. mapping包
mapping 包是一个非常重要的包,它定义了 MyBatis 中众多的解析实体类。这些实体类有一些与SQL语句相关,有一些与 SQL的输入/输出参数有关,有一些与配置信息有关。
mapping包主要完成以下功能。
- SQL语句处理功能;
- 输出结果处理功能;
- 输入参数处理功能;
- 多数据库种类处理功能;
- 其他功能。
1.1 SQL语言处理功能
在 mapping 包中,与 SQL 语句处理功能相关的类主要有三个,MappedStatement类、SqlSource类和 BoundSql类。
- MappedStatement类表示的是数据库操作节点(select、insert、update、delete四类节点)内的所有内容;
- SqlSource类是数据库操作标签中包含的 SQL语句;
- BoundSql类则是 SqlSource类进一步处理的产物。
1.1.1 MappedStatement类
MappedStatement是一个典型的解析实体类,它就是映射文件中数据库操作节点对应的实体。
<insert id="addUser" parameterType="com.github.yeecode.mybatisdemo.model.User">
INSERT INTO `user`
(`name`,`email`,`age`,`sex`,`schoolName`)
VALUES
(#{name},#{email},#{age},#{sex},#{schoolName})
</insert>
1.1.2 SqlSource类
SqlSource是一个解析实体接口,它对应了 MappedStatement中的 SQL语句。SqlSource 本身是一个接口,接口中只定义了一个用以返回一个 BoundSql 对象的方法。
/**
* 一共有四个实现
*/
public interface SqlSource {
/**
* 获取一个BoundSql对象
* @param parameterObject 参数对象
* @return BoundSql对象
*/
BoundSql getBoundSql(Object parameterObject);
}
SqlSource接口的四种实现类的区别如下。
- DynamicSqlSource:动态 SQL语句。所谓动态 SQL是指含有动态 SQL节点(如“if”节点)或者含有“${}”占位符的语句。
- RawSqlSource:原生 SQL语句。指非动态语句,语句中可能含“#{}”占位符,但不含有动态SQL节点,也不含有“${}”占位符。
- StaticSqlSource:静态语句。语句中可能含有“?”,可以直接提交给数据库执行。
- ProviderSqlSource:上面的几种都是通过 XML 文件获取的 SQL 语句,而ProviderSqlSource是通过注解映射的形式获取的 SQL语句。
而 DynamicSqlSource 和 RawSqlSource 都会被处理成 StaticSqlSource,然后再通过StaticSqlSource的 getBoundSql方法得到 SqlSource对象。DynamicSqlSource和 RawSqlSource都在 scripting包中,下章详细介绍 SqlSource接口的四个实现类之间的转化过程。
1.1.3 BoundSql类
BoundSql是参数绑定完成后的 SQL语句。
/**
* 绑定的SQL,是从SqlSource而来,将动态内容都处理完成得到的SQL语句字符串,其中包括?,还有绑定的参数
*/
public class BoundSql {
// 可能含有“?”占位符的sql语句
private final String sql;
// 参数映射列表
private final List<ParameterMapping> parameterMappings;
// 实参对象本身
private final Object parameterObject;
// 实参
private final Map<String, Object> additionalParameters;
// additionalParameters的包装对象
private final MetaObject metaParameters;
}
BoundSql是 SQL语句中一个重要的中间产物,它既存储了转化结束的 SQL信息,又包含了实参信息和一些附加的环境信息。接下来,它会在 SQL的执行中继续发挥作用。
1.2 输出结果处理功能
在映射文件的数据库操作节点中,可以直接使用 resultType设置将输出结果映射为 Java对象。不过,还有一种更为灵活和强大的方式,那就是使用 resultMap来定义输出结果的映射方式。resultMap的功能十分强大,它支持输出结果的组装、判断、懒加载等。
<resultMap id="userMap" type="User" autoMapping="false">
<result property="id" column="id"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
resultMap可以根据结果对象中 sex属性的不同输出不同的子类。
在输出结果的处理中主要涉及 ResultMap类、ResultMapping类、Discriminator类,它们也都是解析实体类。
1.2.1 ResultMap类
ResultMap类就是 resultMap节点对应的解析实体类,其属性和 resultMap节点的信息高度一致。
public class ResultMap {
// 全局配置信息
private Configuration configuration;
// resultMap的编号
private String id;
// 最终输出结果对应的Java类
private Class<?> type;
// XML中的<result>的列表,即ResultMapping列表
private List<ResultMapping> resultMappings;
// XML中的<id>的列表
private List<ResultMapping> idResultMappings;
// XML中的<constructor>中各个属性的列表
private List<ResultMapping> constructorResultMappings;
// XML中非<constructor>相关的属性列表
private List<ResultMapping> propertyResultMappings;
// 所有参与映射的数据库中字段的集合
private Set<String> mappedColumns;
// 所有参与映射的Java对象属性集合
private Set<String> mappedProperties;
// 鉴别器
private Discriminator discriminator;
// 是否存在嵌套映射
private boolean hasNestedResultMaps;
// 是否存在嵌套查询
private boolean hasNestedQueries;
// 是否启动自动映射
private Boolean autoMapping;
对照 XML配置后,所有的属性都比较好理解。稍显繁复的就是有四个*ResultMappings列表。
<resultMap id="userMapByConstructor" type="User">
<constructor>
<idArg column="id" javaType="Integer"/>
<arg column="name" javaType="String"/>
<arg column="sex" javaType="Integer"/>
<arg column="schoolName" javaType="String"/>
</constructor>
</resultMap>
<resultMap id="girlUserMap" type="Girl" extends="userMap">
<result property="email" column="email"/>
</resultMap>
<resultMap id="userMap" type="User" autoMapping="false">
<id property="id" column="id" javaType="Integer" jdbcType="INTEGER"
typeHandler="org.apache.ibatis.type.IntegerTypeHandler"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
<resultMap id="boyUserMap" type="Boy" extends="userMap">
<result property="age" column="age"/>
</resultMap>
在“id=“userMap””的 resultMap中 MyBatis会调用类的无参构造方法创建一个对象,然后再给各个属性赋值。而“id=“userMapByConstructor””的 resultMap中 MyBatis会调用对应的构造方法创建对象。于是,对象的属性被分为了两类:构造方法中的属性和非构造方法中的属性。constructor标签下可以设置一个 idArg标签。普通的 resultMap标签下也可以设置一个id 标签。与其他标签对应的属性不同,这两个标签对应的属性可以作为区别对象是否为同一个对象的标识属性。于是,对象的属性被分为了两类:id属性和非 id属性。
根据以上两种分类方式就产生了下面的四种属性。
- resultMappings:所有的属性;
- idResultMappings:所有的 id属性;
- constructorResultMappings:所有构造方法中的属性;
- propertyResultMappings:所有非构造方法中的属性。
1.2.2 ResultMapping类
ResultMapping中存在大量的属性,因此创建 ResultMapping对象非常复杂。为了改善这个过程,ResultMapping使用了建造者模式。并且,它的建造者直接放在了类的内部,作为内部静态类出现。内部静态类中方法的调用不需要创建类的对象,而它们却可以生成类的对象。构建者方法可以方便地创建一个 ResultMapping对象,并设置各种属性。
基于内部类的建造者模式提升了类的内聚性,值得我们在软件设计时借鉴。
1.2.3 Discriminator
Discriminator是 resultMap内部的鉴别器,就像程序中的选择语句一样,它使得数据查询结果能够根据某些条件的不同而进行不同的映射。
<resultMap id="userMap" type="User" autoMapping="false">
<result property="id" column="id"/>
<result property="name" column="name"/>
<discriminator javaType="int" column="sex">
<case value="0" resultMap="boyUserMap"/>
<case value="1" resultMap="girlUserMap"/>
</discriminator>
</resultMap>
<resultMap id="girlUserMap" type="Girl" extends="userMap">
<result property="email" column="email"/>
</resultMap>
<resultMap id="boyUserMap" type="Boy" extends="userMap">
<result property="age" column="age"/>
</resultMap>
“id=“userMap””的 resultMap能够根据 sex字段的值进行不同的映射:如果 sex值为 0,则最终输出结果为 Girl对象,并且根据查询结果设置email属性;如果 sex值为 1,则最终输出结果为 Boy对象,并且根据查询结果设置 age属性。
public class Discriminator {
// 存储条件判断行的信息,如<discriminator javaType="int" column="sex">中的信息
private ResultMapping resultMapping;
// 存储选择项的信息,键为value值,值为resultMap值。如<case value="0" resultMap="boyUserMap"/>中的信息
private Map<String, String> discriminatorMap;
相比于 Discriminator类的属性,我们更关心它的生效逻辑。在 DefaultResultSetHandler类的resolveDiscriminatedResultMap方法中可以看到这部分逻辑。
/**
* 应用鉴别器
* @param rs 数据库查询出的结果集
* @param resultMap 当前的ResultMap对象
* @param columnPrefix 属性的父级前缀
* @return 已经不包含鉴别器的新的ResultMap对象
* @throws SQLException
*/
public ResultMap resolveDiscriminatedResultMap(ResultSet rs, ResultMap resultMap, String columnPrefix) throws SQLException {
// 已经处理过的鉴别器
Set<String> pastDiscriminators = new HashSet<>();
Discriminator discriminator = resultMap.getDiscriminator();
while (discriminator != null) {
// 求解条件判断的结果,这个结果值就是鉴别器鉴别的依据
final Object value = getDiscriminatorValue(rs, discriminator, columnPrefix);
// 根据真实值判断属于哪个分支
final String discriminatedMapId = discriminator.getMapIdFor(String.valueOf(value));
// 从接下来的case里面找到这个分支
if (configuration.hasResultMap(discriminatedMapId)) {
// 找出指定的resultMap
resultMap = configuration.getResultMap(discriminatedMapId);
// 继续分析下一层
Discriminator lastDiscriminator = discriminator;
// 查看本resultMap内是否还有鉴别器
discriminator = resultMap.getDiscriminator();
// 辨别器出现了环
if (discriminator == lastDiscriminator || !pastDiscriminators.add(discriminatedMapId)) {
break;
}
} else {
break;
}
}
return resultMap;
}
查看判断条件的求解过程,该过程在 DefaultResultSetHandler 类的getDiscriminatorValue方法中。
/**
* 求解鉴别器条件判断的结果
* @param rs 数据库查询出的结果集
* @param discriminator 鉴别器
* @param columnPrefix
* @return 计算出鉴别器的value对应的真实结果
* @throws SQLException
*/
private Object getDiscriminatorValue(ResultSet rs, Discriminator discriminator, String columnPrefix) throws SQLException {
final ResultMapping resultMapping = discriminator.getResultMapping();
// 要鉴别的字段的typeHandler
final TypeHandler<?> typeHandler = resultMapping.getTypeHandler();
// prependPrefix(resultMapping.getColumn(), columnPrefix) 得到列名,然后取出列的值
return typeHandler.getResult(rs, prependPrefix(resultMapping.getColumn(), columnPrefix));
}
1.2.4 输入参数处理功能
MyBatis不仅可以将数据库结果映射为对象,还能够将对象映射成 SQL语句需要的输入参数。这种映射关系由 parameterMap标签来表示。这样,只要输入 User对象,parameterMap就可以将其拆解为 name、schoolName参数。
<parameterMap id="userParam01" type="User">
<parameter property="name" javaType="String"/>
<parameter property="schoolName" javaType="String"/>
</parameterMap>
在输入参数的处理过程中,主要涉及 ParameterMap、ParameterMapping这两个类,它们也都是解析实体类。
作为解析实体类,ParameterMap类和 ParameterMapping类与标签中的属性相对应,整体架构比较简单。并且这两个类和 ResultMap类、ResultMapping类十分类似。
1.2.4 多数据库种类处理功能
作为一个出色的 ORM 框架,MyBatis 支持多种数据库,如 SQL Server、DB2、Oracle、MySQL、PostgreSQL 等。然而,不同类型的数据库之间支持的 SQL 规范略有不同。例如,同样是限制查询结果的条数,在 SQL Server中要使用 TOP关键字,而在 MySQL中要使用 LIMIT关键字。为了能够兼容不同数据库的 SQL规范,MyBatis支持多种数据库。在使用多种数据库前,需要先在配置文件中列举要使用的数据库类型。
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql" />
<property name="SQL Server" value="sqlserver" />
</databaseIdProvider>
<select id="selectByAge" resultMap="userMap" databaseId="mysql">
SELECT * FROM `user` WHERE `age` = #{age} TOP 5
</select>
<select id="selectByAge" resultMap="userMap" databaseId="sqlserver">
SELECT * FROM `user` WHERE `age` = #{age} LIMIT 5
</select>
这样,MyBatis会在连接不同的数据库时使用不同的查询语句。在本例中,MyBatis会在连接 MySQL数据库时使用包含“TOP 5”的查询语句,在连接 SQL Server数据库时使用包含“LIMIT 5”的语句。
多数据支持的实现由 DatabaseIdProvider接口负责。它有一个 VendorDatabaseIdProvider子类,还有一个即将废弃的 DefaultDatabaseIdProvider 子类。
/**
* 获取当前的数据源类型的别名
* @param dataSource 数据源
* @return 数据源类型别名
* @throws SQLException
*/
private String getDatabaseName(DataSource dataSource) throws SQLException {
// 获取当前连接的数据库名
String productName = getDatabaseProductName(dataSource);
// 如果设置有properties值,则根据将获取的数据库名称作为模糊的key,映射为对应的value
if (this.properties != null) {
for (Map.Entry<Object, Object> property : properties.entrySet()) {
if (productName.contains((String) property.getKey())) {
return (String) property.getValue();
}
}
// 没有找到对应映射
return null;
}
return productName;
}
getDatabaseName方法做了两个工作,首先是获取当前数据源的类型,然后是将数据源类型映射为我们在 databaseIdProvider节点中设置的别名。这样,在需要执行 SQL语句时,就可以根据数据库操作节点中的 databaseId设置对 SQL语句进行筛选。
1.2.5 其他功能
mapping包中还有两个重要的类:Environment类和 CacheBuilder类。Environment类也是一个解析实体类,它对应了配置文件中的environments节点。
public final class Environment {
// 编号
private final String id;
// 事务工厂
private final TransactionFactory transactionFactory;
// 数据源信息
private final DataSource dataSource;
CacheBuilder 类是缓存建造者,它负责完成缓存对象的创建。
mapping包中还存在一些枚举类
- FetchType:延迟加载设置;
- ParameterMode:参数类型,指输入参数、输出参数等;
- ResultFlag:返回结果中属性的特殊标志,表示是否为 id属性、是否为构造器属性;
- ResultSetType:结果集支持的访问方式;
- SqlCommandType:SQL命令类型,指增、删、改、查等;
- StatementType:SQL语句种类,指是否为预编译的语句、是否为存储过程等。