mybatis是目前非常流行的ORM框架,在其基础上还衍生了mybatis-plus)等优秀的框架。mybatis简单,易用,易扩展。
接下来我们用一个简单的例子(几乎包含所有配置)来看一下mybatis相关的配置项
下面是一个包含用户、商品、订单的DAO例子,关系图如下:
mybatis的配置
MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。 各属性介绍可以参考[官网](mybatis – MyBatis 3 | 配置)
此示例项目的mybatis-config.xml
内容如下
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 定义属性值 -->
<properties>
<property name="username" value="root"/>
<property name="id" value="development"/>
</properties>
<!-- 全局配置信息 -->
<!-- 一个配置完整的 settings 元素的示例如下:-->
<settings>
<!-- 全局性地开启或关闭所有映射器配置文件中已配置的任何缓存。-->
<setting name="cacheEnabled" value="true"/>
<!-- 延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。 特定关联关系中可通过设置 fetchType 属性来覆盖该项的开关状态。。-->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 开启时,任一方法的调用都会加载该对象的所有延迟加载属性。 否则,每个延迟加载属性会按需加载(参考 lazyLoadTriggerMethods)。(在 3.4.1 及之前的版本中默认为 true) -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允许单个语句返回多结果集(需要数据库驱动支持)。 -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 使用列标签代替列名。实际表现依赖于数据库驱动,具体可参考数据库驱动的相关文档,或通过对比测试来观察。 -->
<setting name="useColumnLabel" value="true"/>
<!-- 允许 JDBC 支持自动生成主键,需要数据库驱动支持。如果设置为 true,将强制使用自动生成主键。尽管一些数据库驱动不支持此特性,但仍可正常工作(如 Derby)。 -->
<setting name="useGeneratedKeys" value="false"/>
<!-- 指定 MyBatis 应如何自动映射列到字段或属性。 NONE 表示关闭自动映射;PARTIAL 只会自动映射没有定义嵌套结果映射的字段。 FULL 会自动映射任何复杂的结果集(无论是否嵌套)。 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 指定发现自动映射目标未知列(或未知属性类型)的行为。
NONE: 不做任何反应
WARNING: 输出警告日志('org.apache.ibatis.session.AutoMappingUnknownColumnBehavior' 的日志等级必须设置为 WARN)
FAILING: 映射失败 (抛出 SqlSessionException) --> <setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<!-- 配置默认的执行器。SIMPLE 就是普通的执行器;REUSE 执行器会重用预处理语句(PreparedStatement); BATCH 执行器不仅重用语句还会执行批量更新。 -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 设置超时时间,它决定数据库驱动等待数据库响应的秒数。 -->
<setting name="defaultStatementTimeout" value="60"/>
<!-- 为驱动的结果集获取数量(fetchSize)设置一个建议值。此参数只可以在查询设置中被覆盖。 -->
<setting name="defaultFetchSize" value="100"/>
<!-- 指定语句默认的滚动策略。
FORWARD_ONLY | SCROLL_SENSITIVE | SCROLL_INSENSITIVE | DEFAULT(等同于未设置)-->
<setting name="defaultResultSetType" value="DEFAULT"/>
<!-- 是否允许在嵌套语句中使用分页(RowBounds)。如果允许使用则设置为 false。 -->
<setting name="safeRowBoundsEnabled" value="false"/>
<!-- 是否允许在嵌套语句中使用结果处理器(ResultHandler)。如果允许使用则设置为 false。 -->
<setting name="safeResultHandlerEnabled" value="true"/>
<!-- 是否开启驼峰命名自动映射,即从经典数据库列名 A_COLUMN 映射到经典 Java 属性名 aColumn。 -->
<setting name="mapUnderscoreToCamelCase" value="false"/>
<!-- MyBatis 利用本地缓存机制(Local Cache)防止循环引用和加速重复的嵌套查询。 默认值为 SESSION,会缓存一个会话中执行的所有查询。
若设置值为 STATEMENT,本地缓存将仅用于执行语句,对相同 SqlSession 的不同查询将不会进行缓存。 -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 当没有为参数指定特定的 JDBC 类型时,空值的默认 JDBC 类型。 某些数据库驱动需要指定列的 JDBC 类型,多数情况直接用一般类型即可,比如 NULL、VARCHAR 或 OTHER。 -->
<setting name="jdbcTypeForNull" value="OTHER"/>
<!-- 指定对象的哪些方法触发一次延迟加载。 -->
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
<!-- 指定动态 SQL 生成使用的默认脚本语言。 -->
<setting name="defaultScriptingLanguage" value="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver"/>
<!-- 指定 Enum 使用的默认 TypeHandler 。 -->
<setting name="defaultEnumTypeHandler" value="org.apache.ibatis.type.EnumTypeHandler"/>
<!-- 指定当结果集中值为 null 的时候是否调用映射对象的 setter(map 对象时为 put)方法,这在依赖于 Map.keySet() 或 null 值进行初始化时比较有用。注意基本类型(int、boolean 等)是不能设置成 null 的。 -->
<setting name="callSettersOnNulls" value="false"/>
<!-- 当返回行的所有列都是空时,MyBatis默认返回 null。 当开启这个设置时,MyBatis会返回一个空实例。 请注意,它也适用于嵌套的结果集(如集合或关联)。 -->
<setting name="returnInstanceForEmptyRow" value="false"/>
<!-- 指定 MyBatis 增加到日志名称的前缀。 -->
<setting name="logPrefix" value="learn_mybatis_log_"/>
<!-- 指定 MyBatis 所用日志的具体实现,未指定时将自动查找。 -->
<!-- <setting name="logImpl"--> <!-- value="SLF4J | LOG4J | LOG4J2 | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING"/>-->
<!-- 指定 Mybatis 创建可延迟加载对象所用到的代理工具。 CGLIB | JAVASSIST--> <setting name="proxyFactory" value="JAVASSIST"/>
<!-- 指定 VFS 的实现 -->
<!-- <setting name="vfsImpl" value="org.mybatis.example.YourselfVfsImpl"/> --> <!-- 允许使用方法签名中的名称作为语句参数名称。 为了使用该特性,你的项目必须采用 Java 8 编译,并且加上 -parameters 选项。(新增于 3.4.1) -->
<setting name="useActualParamName" value="true"/>
<!-- 指定一个提供 Configuration 实例的类。 这个被返回的 Configuration 实例用来加载被反序列化对象的延迟加载属性值。 这个类必须包含一个签名为static Configuration getConfiguration() 的方法。(新增于 3.2.3) -->
<!-- <setting name="configurationFactory" value="org.mybatis.example.ConfigurationFactory"/> --> <!-- 从SQL中删除多余的空格字符。请注意,这也会影响SQL中的文字字符串。 -->
<setting name="shrinkWhitespacesInSql" value="true"/>
<!-- 指定一个拥有 provider 方法的 sql provider 类 (新增于 3.5.6). 这个类适用于指定 sql provider 注解上的type(或 value) 属性(当这些属性在注解中被忽略时)。 (e.g. @SelectProvider) --> <!-- <setting name="defaultSqlProviderType" value="true"/> -->
<!-- 为 'foreach' 标签的 'nullable' 属性指定默认值。 -->
<setting name="nullableOnForEach" value="false"/>
<!-- 当应用构造器自动映射时,参数名称被用来搜索要映射的列,而不再依赖列的顺序。-->
<setting name="argNameBasedConstructorAutoMapping" value="false"/>
</settings>
<typeAliases>
<!-- 配置别名信息 -->
<typeAlias alias="CustomerEntity" type="com.mingshashan.mybatis.learn.entity.CustomerEntity"/>
<typeAlias alias="OrderEntity" type="com.mingshashan.mybatis.learn.entity.OrderEntity"/>
<typeAlias alias="OrderItemEntity" type="com.mingshashan.mybatis.learn.entity.OrderItemEntity"/>
<typeAlias alias="ProductEntity" type="com.mingshashan.mybatis.learn.entity.ProductEntity"/>
<typeAlias alias="TagEntity" type="com.mingshashan.mybatis.learn.entity.TagEntity"/>
<typeAlias alias="CustomCache" type="com.mingshashan.mybatis.learn.mybatis.CustomCache"/>
</typeAliases>
<typeHandlers>
<typeHandler handler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"/>
</typeHandlers>
<!-- 每次 MyBatis 创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。
默认的对象工厂需要做的仅仅是实例化目标类,要么通过默认无参构造方法, 要么通过存在的参数映射来调用带有参数的构造方法。 如果想覆盖对象工厂的默认行为,可以通过创建自己的对象工厂来实现。 -->
<objectFactory type="com.mingshashan.mybatis.learn.mybatis.CustomObjectFactory"/>
<!-- MyBatis 提供在构造对象的时候,对于指定的对象进行特殊的加工,其配置方式如下 -->
<objectWrapperFactory type="com.mingshashan.mybatis.learn.mybatis.CustomMapWrapperFactory"/>
<reflectorFactory type="com.mingshashan.mybatis.learn.mybatis.CustomReflectorFactory"/>
<environments default="dev">
<environment id="dev">
<!-- 配置事务管理器的类型 -->
<transactionManager type="JDBC"/>
<!-- 配置数据源的类型,以及数据库连接的相关信息 -->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.75.138:3306/dev?useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="000000"/>
</dataSource>
</environment>
<environment id="test">
<!-- 配置事务管理器的类型 -->
<transactionManager type="JDBC"/>
<!-- 如果environment id为test则使用JNDI -->
<dataSource type="JNDI">
<property name="data_source" value="jdbc/mybatis-jndi"/>
<property name="initial_context" value="java:/comp/env"/>
</dataSource>
</environment>
</environments>
<databaseIdProvider type="DB_VENDOR">
<property name="SQL Server" value="sqlserver"/>
<property name="DB2" value="db2"/>
<property name="Oracle" value="oracle"/>
<property name="Mysql" value="mysql"/>
</databaseIdProvider>
<!-- 配置映射配置文件的位置 -->
<mappers>
<mapper resource="mapper/CustomerMapper.xml"/>
<mapper resource="mapper/OrderItemMapper.xml"/>
<mapper resource="mapper/OrderMapper.xml"/>
<mapper resource="mapper/ProductMapper.xml"/>
<mapper resource="mapper/TagMapper.xml"/>
</mappers>
</configuration>
下面是mapper配置,即xml映射文件的配置,我们只看其中的CustomerMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mingshashan.mybatis.learn.dao.mapper.CustomerMapper">
<!--
cache- 配置本定命名空间的缓存。
type- cache实现类,默认为PERPETUAL,可以使用自定义的cache实现类(别名或完整类名皆可)
eviction- 回收算法,默认为LRU,可选的算法有:
LRU– 最近最少使用的:移除最长时间不被使用的对象。
FIFO– 先进先出:按对象进入缓存的顺序来移除它们。
SOFT– 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK– 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
flushInterval- 刷新间隔,默认为1个小时,单位毫秒
size- 缓存大小,默认大小1024,单位为引用数
readOnly- 只读
-->
<cache type="com.mingshashan.mybatis.learn.mybatis.CustomCache"
size="2048" flushInterval="60000">
<property name="cacheFile" value="./cache.tmp"/>
</cache>
<!--
cache-ref–从其他命名空间引用缓存配置。
如果你不想定义自己的cache,可以使用cache-ref引用别的cache。因为每个cache都以namespace为id,所以cache-ref只需要配置一个namespace属性就可以了。需要注意的是,如果cache-ref和cache都配置了,以cache为准。
-->
<!-- <cache-ref namespace="com.someone.application.data.SomeMapper"/> -->
<resultMap id="CustomerMap" type="com.mingshashan.mybatis.learn.entity.CustomerEntity">
<id property="id" column="id"></id>
<result property="name" column="name"></result>
<result property="gender" column="gender"></result>
<result property="phone" column="phone"></result>
<result property="addressList" column="address_info"
typeHandler="com.mingshashan.mybatis.learn.mybatis.handler.AddressInfoHandler"
javaType="java.util.List"
jdbcType="VARCHAR"
></result>
<result property="gmtCreated" column="gmt_created"></result>
<result property="gmtModified" column="gmt_modified"></result>
<result property="isValid" column="is_valid"></result>
<collection property="tagEntityList" ofType="TagEntity">
<id property="id" column="id"/>
<result property="name" column="name"/>
</collection>
</resultMap>
<sql id="customer_column">
id, name, gender, phone, address_info, gmt_created, gmt_modified
</sql>
<insert id="saveCustomer" parameterType="CustomerEntity">
INSERT INTO T_ARG_CUSTOMER(
<include refid="customer_column"></include>
)
values(#{id}, #{name}, #{gender}, #{phone}, #{addressList}, #{gmtCreated}, #{gmtModified})
</insert>
<update id="updateCustomer" parameterType="CustomerEntity">
update T_ARG_CUSTOMER set
name = #{name},
gender = #{gender},
phone = #{phone},
address_info = #{addressList},
gmt_modified = #{gmtModified}
from t_arg_customer
<where>
ID = #{id}
</where>
</update>
<update id="deleteCustomerById">
update T_ARG_CUSTOMER set is_valid = '0'
<where>
id = #{id}
</where>
</update>
<select id="findById" resultType="CustomerEntity">
select
<include refid="customer_column"></include>
from T_ARG_CUSTOMER
<where>
id = #{id}
</where>
</select>
</mapper>
Mybatis的启动加载
整个过程的时序图大概如下
先是XMLConfigBuilder
再是XMLMapperBuilder
然后结合着代码来分析
1.SqlSessionFactoryBuilder
接下来看一下mybatis的启动加载过程,从mybatis的官方文档看mybatis的启动加载入口代码示例:
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
可以知道,其入口就是new SqlSessionFactoryBuilder().build(inputStream)
。接下来我们来分析一下其启动加载过程。
代码如下:
public class SqlSessionFactoryBuilder {
// 方法入口
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 构造XMLConfigBuilder,将解析逻辑交给XMLConfigBuilder去处理
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 执行解析逻辑。解析完后构造DefaultSqlSessionFactory
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
}
2.XMLConfigBuilder
而parse
方法会调用parseConfiguration
方法解析mybatis的configuration配置
public class XMLConfigBuilder extends BaseBuilder {
// 解析配置,即将mybatis-config.xml的xml配置转换为对应的mybatis模型配置
private void parseConfiguration(XNode root) {
try {
// properties标签
propertiesElement(root.evalNode("properties"));
// settings标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
// vfs配置
loadCustomVfs(settings);
// 日志配置
loadCustomLogImpl(settings);
// 别名配置
typeAliasesElement(root.evalNode("typeAliases"));
// 插件配置
pluginElement(root.evalNode("plugins"));
// 自定义的objectFactory配置
objectFactoryElement(root.evalNode("objectFactory"));
// 自定义的objectWrapperFactory配置
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 自定义的reflectorFactory配置
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 其他的settings配置
settingsElement(settings);
// 在objectFactory and objectWrapperFactory初始化后,配置environments
environmentsElement(root.evalNode("environments"));
// databaseIdProvider
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// typeHandlers
typeHandlerElement(root.evalNode("typeHandlers"));
// 所有的mappers
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
}
3.XmlMapperBuilder
可以看到mapperElement会调用XmlMapperBuilder的parse方法去解析处理,其主要代码如下:
public class XMLMapperBuilder extends BaseBuilder {
public void parse() {
//先从loadedResources判断是不是已经加载过
if (!configuration.isResourceLoaded(resource)) {
// 如果没有加载过,则去处理mapper标签下面的元素
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps(); // 解析待定ResultMaps
parsePendingCacheRefs(); // 解析待定ChacheRefs
parsePendingStatements(); // 解析待定Statements
}
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
// 用到了MapperBuilderAssistant
builderAssistant.setCurrentNamespace(namespace);
// 处理cache-ref
cacheRefElement(context.evalNode("cache-ref"));
// 处理cache
cacheElement(context.evalNode("cache"));
// 处理parameterMap
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 处理resultMap
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 处理sql
sqlElement(context.evalNodes("/mapper/sql"));
// "select|insert|update|delete" 这些statement的处理
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
关于上面的
parsePendingResultMaps(); // 解析待定ResultMaps
parsePendingCacheRefs(); // 解析待定ChacheRefs
parsePendingStatements(); // 解析待定Statements
可以参考:could not find a parent resultmap with id
XMLMapperBuilder.parse() -> 存在resultMap继承,父resultMap还未加载的,抛出Could not find a parent resultmap with id xx,
被上层XMLMapperBuilder.resultMapElement()方法捕获,
存储到 this.configuration.addIncompleteResultMap()即 configuration.IncompleteResultMap 列表内,属于不完全加载,
每一次parse循环会执行 this.parsePendingResultMaps() 检验IncompleteResultMap的父(extends) resultMap是否被加载进来,如果上一次parse加载了,这次就可以处理IncompleteResultMap进行完全加载.
由于每次都要循环检验,毕竟有消耗,因此为避免这种消耗,是否可以让父resultMap优先加载呢,这样后面就不会存在IncompleteResultMap , 需要把父mapper.xml放目录结构父层级 或者 以AxxMapper.xml开头排同层级之前(注意:这并不能解决后面checkDaoConfig报错问题)
4.代码调试过程
SqlSessionFactory的build方法为mybatis配置加载的入口
然后XMLConfigBuilder的parseConfiguration解析mybatis-config.xml的配置
接着调用XMLMapperBuilder的parse方法解析所有的mapper.xml文件
其parse方法执行如下:
之后调用XMLStatementBuilder的parseStatementNode方法
XMLStatementBuilder会交给MapperBuilderAssistant的addMappedStatement方法由MappedStatement的builder模式构造出MappedStatement
再将构造出的MappedStatement放到Configuration的mappedStatements内部map容器中
5.Mybatis配置的实例化对象Configuration
整个mybatis的配置会保存在org.apache.ibatis.session.Configuration
对象(一个全局的实例)
而可以从SqlSessionFactory接口获取到Configuration,其关系图如下:
DefaultSqlSessionFactory会持有Configuration对象
public class DefaultSqlSessionFactory implements SqlSessionFactory {
private final Configuration configuration;
public DefaultSqlSessionFactory(Configuration configuration) {
this.configuration = configuration;
}
}
Configuration的结构如下图所示:
通过上面的配置信息,代码调试及代码执行流程,可以看到mybatis整个启动加载过程会解析mybatis配置里面的所有信息。
项目工程地址:learn-mybatis