Mapper映射文件的配置
文章目录
一:更新的配置和使用
1:模板mapper
看到insert, update, delete我们就知道其作用了,顾名思义嘛,myabtis 作为持久层框架,必须要对CRUD啊
先来看看 insert, update, delete 怎么配置, 能配置哪些元素吧:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<!-- mapper 为根元素节点, 一个namespace对应一个dao -->
<!--
Mapper元素只有一个属性namespace,它有两个作用:
一是用于区分不同的mapper(在不同的mapper文件里,子元素的id可以相同,mybatis通过namespace和子元素的id联合区分)
二是与接口关联(应用程序通过接口访问mybatis时,mybatis通过接口的完整名称查找对应的mapper配置,因此namespace的命名务必小心一定要某接口同名)。
-->
<mapper namespace="com.dy.dao.UserDao">
<!--
cache- 配置本定命名空间的缓存。
参数1:type- cache实现类,默认为PERPETUAL,可以使用自定义的cache实现类(别名或完整类名皆可)
参数2:eviction- 回收算法,默认为LRU,可选的算法有:
LRU– 最近最少使用的:移除最长时间不被使用的对象。
FIFO– 先进先出:按对象进入缓存的顺序来移除它们。
SOFT– 软引用:移除基于垃圾回收器状态和软引用规则的对象。
WEAK– 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。
参数3:flushInterval- 刷新间隔,默认为1个小时,单位毫秒
参数4:size- 缓存大小,默认大小1024,单位为引用数
参数5:readOnly- 只读
-->
<cache type="PERPETUAL" eviction="LRU" flushInterval="60000" size="512" readOnly="true" />
<!--
cache-ref–从其他命名空间引用缓存配置。
如果你不想定义自己的cache,可以使用cache-ref引用别的cache。
因为每个cache都以namespace为id,所以cache-ref只需要配置一个namespace属性就可以了。
需要注意的是,如果cache-ref和cache都配置了,以cache为准。
-->
<cache-ref namespace="com.someone.application.data.SomeMapper"/>
<insert
id="insertUser"
parameterType="com.demo.User"
flushCache="true"
statementType="PREPARED"
keyProperty=""
keyColumn=""
useGeneratedKeys="false"
timeout="20">
<update
id="updateUser"
parameterType="com.demo.User"
flushCache="true"
statementType="PREPARED"
timeout="20">
<delete
id="deleteUser"
parameterType="com.demo.User"
flushCache="true"
statementType="PREPARED"
timeout="20">
</mapper>
id - (必须配置)
id是命名空间中的唯一标识符,可被用来代表这条语句。
一个命名空间(namespace) 对应一个dao接口, 这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致
parameterType (可选配置, 默认为mybatis自动选择处理)
将要传入语句的参数的完全限定类名或别名
如果不配置,mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理
parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象)
flushCache (可选配置,默认配置为true)
将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:true(对应插入、更新和删除语句)
statementType (可选配置,默认配置为PREPARED)
STATEMENT,PREPARED 或 CALLABLE 的一个。
这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
keyProperty (可选配置, 默认为unset)
仅对 insert 和 update 有用 - 唯一标记一个属性
MyBatis 会通过 getGeneratedKeys 的返回值或者通过 insert 语句的 selectKey 子元素设置它的键值,默认:unset。
如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。
keyColumn (可选配置)
仅对 insert 和 update 有用,通过生成的键值设置表中的列名
这个设置仅在某些数据库(像 PostgreSQL)是必须的,当主键列不是表中的第一列的时候需要设置。
如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表
useGeneratedKeys (可选配置, 默认为false)
仅对 insert 和 update 有用
这会令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法来取出由数据库内部生成的主键,默认值:false。
timeout (可选配置, 默认为unset, 依赖驱动)
这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)
2:实例说明
以上就是一个模板配置, 哪些是必要配置,哪些是根据自己实际需求,看一眼就知道了。看一个真实的UserDao-Mapper.xml
配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"
"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">
<mapper namespace="com.dy.dao.UserDao">
<!-- 对应userDao中的insertUser方法, -->
<insert id="insertUser" parameterType="com.dy.entity.User">
insert into user(id, name, password, age, deleteFlag)
values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})
</insert>
<!-- 对应userDao中的updateUser方法 -->
<update id="updateUser" parameterType="com.dy.entity.User">
update user set name = #{name}, password = #{password}, age = #{age}, deleteFlag = #{deleteFlag}
where id = #{id};
</update>
<!-- 对应userDao中的deleteUser 方法 -->
<delete id="deleteUser" parameterType="com.dy.entity.User">
delete from user where id = #{id};
</delete>
</mapper>
这样,一个简单的映射关系就建立了。仔细观察上面parameterType => "com.dy.entity.User"
,会发现要是包名很长,如果每次都这么写,非常麻烦,此时typeAliases就派上用场了。
<typeAliases>
<!--
通过package, 可以直接指定package的名字, mybatis会自动扫描你指定包下面的javabean,
并且默认设置一个别名,默认的名字为: javabean 的首字母小写的非限定类名来作为它的别名。
也可在javabean 加上注解@Alias 来自定义别名, 例如: @Alias(user)
<package name="com.dy.entity"/>
-->
<typeAlias alias="user" type="com.dy.entity.User"/>
</typeAliases>
这样,一个别名就取好了,咱们可以把上面的 com.dy.entity.User 都直接改为user 了。 这多方便呀!
我这儿数据库用的是mysql, 我把user表的主键id 设置了自动增长,以上代码运行正常,那么问题来了:要是换成oracle数据库怎么办? oracle 可是不支持id自增长啊? 怎么办?
<!-- 对应userDao中的insertUser方法, -->
<insert id="insertUser" parameterType="com.dy.entity.User">
<!-- oracle等不支持id自增长的,可根据其id生成策略,先获取id -->
<selectKey resultType="int" order="BEFORE" keyProperty="id">
select seq_user_id.nextval as id from dual
</selectKey>
insert into user(id, name, password, age, deleteFlag)
values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})
</insert>
同理,如果我们在使用mysql的时候,想在数据插入后返回插入的id, 我们也可以使用 selectKey 这个元素
<!-- 对应userDao中的insertUser方法, -->
<insert id="insertUser" parameterType="com.dy.entity.User">
<!-- oracle等不支持id自增长的,可根据其id生成策略,先获取id
<selectKey resultType="int" order="BEFORE" keyProperty="id">
select seq_user_id.nextval as id from dual
</selectKey>
-->
<!-- mysql插入数据后,获取id,该方法LAST_INSERT_ID()与数据库连接绑定,同属统一会话级别。-->
<selectKey keyProperty="id" resultType="int" order="AFTER" >
SELECT LAST_INSERT_ID() as id
</selectKey>
insert into user(id, name, password, age, deleteFlag)
values(#{id}, #{name}, #{password}, #{age}, #{deleteFlag})
</insert>
selectKey给了你一个简单的行为在你的数据库中来处理自动生成的主键,而不需要使你的Java代码变得复杂。
在上面的示例中,selectKey元素将会首先运行,userid会被设置,然后插入语句会被调用。
另外,selectKey节点生成的KeyGenerator优先级高于statement节点的useGeneratedKeys属性生成的KeyGenerator对象
也就是说配置了SelectKey子节点就不需要再配置useGeneratedKeys属性了
二:select、resultMap的配置及使用
select无疑是我们最常用,也是最复杂的,mybatis通过resultMap能帮助我们很好地进行高级映射
下面就开始看看select 以及 resultMap的用法:
1:select的配置
<select
id="selectPerson"
parameterType="int"
resultType="hashmap"
resultMap="personResultMap"
flushCache="false"
useCache="true"
timeout="10000"
fetchSize="256"
statementType="prepared"
resultSetType="forward_only"
>
id - (必须配置)
id是命名空间中的唯一标识符,可被用来代表这条语句。
一个命名空间(namespace) 对应一个dao接口, 这个id也应该对应dao里面的某个方法(相当于方法的实现),因此id 应该与方法名一致
parameterType (可选配置, 默认为mybatis自动选择处理)
将要传入语句的参数的完全限定类名或别名
如果不配置,mybatis会通过ParameterHandler 根据参数类型默认选择合适的typeHandler进行处理
parameterType 主要指定参数类型,可以是int, short, long, string等类型,也可以是复杂类型(如对象)
resultType (resultType & resultMap二选一)
resultType用以指定返回类型,指定的类型可以是基本类型,可以是java容器,也可以是javabean
resultMap (resultType & resultMap二选一)
resultMap用于引用我们通过 resultMap标签定义的映射类型,这也是mybatis组件高级复杂映射的关键
flushCache(可选)
将其设置为 true,任何时候只要语句被调用,都会导致本地缓存和二级缓存都会被清空,默认值:false
useCache(可选)
将其设置为 true,将会导致本条语句的结果被二级缓存,默认值:对 select 元素为 true
statementType (可选配置,默认配置为PREPARED)
STATEMENT,PREPARED 或 CALLABLE 的一个。
这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED
timeout (可选配置, 默认为unset, 依赖驱动)
这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为 unset(依赖驱动)
fatchSize (可选)
这是尝试影响驱动程序每次批量返回的结果行数和这个设置值相等。默认值为 unset(依赖驱动)
resultSetType (可选)
FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一个,默认值为 unset (依赖驱动)
2:实例说明
配置看起来总是这么多,不过实际常用的配置也就那么几个,看一个CourseDao-Mapper.xml配置
<mapper namespace="com.dy.dao.CourseDao">
<!--
1.此处直接将resultType 设置为course, 一看就知道我设置了别名吧,如果没有设置别名,那么resultType = com.dy.entity.Course。
2.可能细心的你会发现:Course.java中的属性名与数据库字段名不一致,下面,我就在sql语句中用了as, 使之匹配,当然方法不止一种,在学习了resultMap之后,你能看到一种更直观优雅的方式去将javabean中的属性与数据库字段名保持一致
3.findCourseById 与CourseDao中findCourseById方法对应, 那么传入的参数名称以及类型也应该保持对应关系。
4.可以看到,在sql语句中,通过#{}表达式可以获取参数。
5.下面这条sql语句,实际上的形式是怎么样的?还记得之前说过,mybatis默认为preparedStatement吧,那么,用我们jdbc代码来看,它其实就是:
select course_id as id, course_name as name, course_delete_flg as deleteFlag from t_course where course_id=?
-->
<select id="findCourseById" resultType="course" >
select course_id as id, course_name as name, course_delete_flg as deleteFlag from t_course where course_id=#{courseId}
</select>
</mapper>
3:resultMap
有个问题值得思考: 一个student可以对应多个course, 那么,在mybatis中如何处理这种一对多, 甚至于多对多,一对一的关系呢?
这就引出了 resultMap 这个东西, mybatis的resultMap功能可谓十分强大,能够处理复杂的关系映射
<!--
resultMap –结果映射,用来描述如何从数据库结果集映射到你想要的对象。
1.type 对应类型,可以是javabean, 也可以是其它
2.id 必须唯一, 用于标示这个resultMap的唯一性,在使用resultMap的时候,就是通过id指定
-->
<resultMap type="" id="">
<!--
id, 唯一性,注意啦,这个id用于标示这个javabean对象的唯一性,不一定会是数据库的主键(不要把它理解为数据库对应表的主键)
property属性对应javabean的属性名,column对应数据库表的列名
(这样,当javabean的属性与数据库对应表的列名不一致的时候,就能通过指定这个保持正常映射了)
-->
<id property="" column=""/>
<!-- result与id相比, 对应普通属性 -->
<result property="" column=""/>
<!-- constructor对应javabean中的构造方法-->
<constructor>
<!-- idArg 对应构造方法中的id参数;-->
<idArg column=""/>
<!-- arg 对应构造方法中的普通参数;-->
<arg column=""/>
</constructor>
<!--
聚集元素用来处理“一对多”的关系。需要指定映射的Java实体类的属性,属性的javaType(一般为ArrayList);
列表中对象的类型ofType(Java实体类);对应的数据库表的列名称;
1:collection,对应javabean中容器类型, 是实现一对多的关键
2:property 为javabean中容器对应字段名
3:column 为体现在数据库中列名
4:ofType 就是指定javabean中容器指定的类型
不同情况需要告诉MyBatis 如何加载一个聚集。MyBatis 可以用两种方式加载:
1. select: 执行一个其它映射的SQL 语句返回一个Java实体类型。较灵活;
2. resultMap: 使用一个嵌套的结果映射来处理通过join查询结果集,映射成Java实体类型。
-->
<collection property="" column="" ofType=""></collection>
<!--
联合元素用来处理“多对一”的关系。需要指定映射的Java实体类的属性,属性的javaType(通常MyBatis 自己会识别)。
对应的数据库表的列名称。如果想覆写的话返回结果的值,需要指定typeHandler。
1:association 为关联关系,是实现N对一的关键。
2:property 为javabean中容器对应字段名
3:column 为体现在数据库中列名
4:javaType 指定关联的类型
不同情况需要告诉MyBatis 如何加载一个联合。MyBatis可以用两种方式加载:
1. select: 执行一个其它映射的SQL 语句返回一个Java实体类型。较灵活;
2. resultMap: 使用一个嵌套的结果映射来处理,通过join查询结果集,映射成Java实体类型。
-->
<association property="" column="" javaType=""></association>
<!--
有时一个单独的数据库查询也许返回很多不同(但是希望有些关联)数据类型的结果集。
鉴别器元素就是被设计来处理这个情况的,还有包括类的继承层次结构。
鉴别器非常容易理解,因为它的表现很像Java语言中的switch语句。
定义鉴别器指定了column和javaType属性。列是MyBatis查找比较值的地方。
JavaType是需要被用来保证等价测试的合适类型(尽管字符串在很多情形下都会有用)。
下面这个例子为,当classId为20000001时,才映射classId属性。
-->
<discriminator column="CLASS_ID" javaType="String" jdbcType="VARCHAR">
<case value="20000001" resultType="liming.student.manager.data.model.StudentEntity" >
<result property="classId" column="CLASS_ID" javaType="String" jdbcType="VARCHAR"/>
</case>
</discriminator>
</resultMap>
一个student对应多个course, 典型的一对多,就来看看mybatis怎么配置这种映射吧:StudentDao-Mapper.xml
<mapper namespace="com.dy.dao.StudentDao">
<!-- 这儿定义一个resultMap -->
<resultMap type="student" id="studentMap">
<!--
数据库中主键是id, 但是我这儿却是指定idCard为主键,为什么?
刚刚讲了,id用来表示唯一性, 我们可以认为只要idCard一样,那么他就是同一个学生。
如果此处用数据库中id, 那么mybatis将会认为数据库中每条记录都是一个student, 这显然不符合逻辑
-->
<id property="idCard" column="stu_id_card"/>
<result property="id" column="stu_id"/>
<result property="name" column="stu_name"/>
<result property="deleteFlag" column="stu_delete_flg"/>
<constructor>
<idArg javaType="String" column="STUDENT_ID"/>
<arg javaType="String" column="STUDENT_NAME"/>
<arg javaType="String" column="STUDENT_SEX"/>
<arg javaType="Date" column="STUDENT_BIRTHDAY"/>
</constructor>
<!--
这儿就是实现一对多的关键。
在Student中,courseList为List<Course>, 因此,ofType也应该与之对应(当然,我用了别名,不然要蛋疼的写全名了)。
collection的子标签是在指定Course的映射关系(由于Course的javabean的属性名与数据库的列名不一致)
-->
<collection property="courseList" column="stu_course_id" ofType="Course">
<id property="id" column="course_id"/>
<result property="name" column="course_name"/>
<result property="deleteFlag" column="course_delete_flg"/>
</collection>
</resultMap>
<!-- 这儿将返回类型设置成了上面指定的studentMap -->
<select id="findStudentById" resultMap="studentMap">
SELECT s.*, c.*
FROM t_student s LEFT JOIN t_course c
ON s.stu_course_id=c.course_id
WHERE s.stu_id_card=#{idCard}
</select>
<!-- sql –可以重用的SQL块,可以被其他数据库操作语句引用。-->
<sql id="userColumns">
userid,username,password
</sql>
<select id="queryUsers" parameterType="UserDto" resultType="UserDto" useCache="false">
select <include refid="userColumns"/>
from t_user t
where t.username = #{username}
</select>
</mapper>
4:字符串代入法和SQL注入
默认的情况下,使用#{}语法会促使MyBatis 生成PreparedStatement 属性并且使用PreparedStatement 的参数(=?)来安全的设置值。
尽量这些是快捷安全,也是经常使用的。
但有时候你可能想直接未更改的字符串代入到SQL 语句中。
比如说,对于ORDER BY,你可能会这样使用:ORDER BY ${columnName}但MyBatis 不会修改和规避掉这个字符串。
这样地接收和应用一个用户输入到未更改的语句中,是非常不安全的。
这会让用户能植入破坏代码,所以,要么要求字段不要允许客户输入,要么你直接来检测他的合法性 。【SQL注入】
三:子元素解析
1:子元素cache解析
Mapper配置文件是由XMLMapperBuilder解析的,其中cacheElement方法负责解析cache元素
它通过调用CacheBuilder的相应方法完成cache的创建。
每个cache内部都有一个唯一的ID,这个id的值就是namespace。
创建好的cache对象存入configuration的cache缓存中
该缓存以cache的ID属性即namespace为key,这里再次体现了mybatis的namespace的强大用处
private void cacheElement(XNode context) throws Exception {
if (context != null) {
// type属性,默认是perpertual
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// eviction属性,默认是LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 其他属性
Long flushInterval = context.getLongAttribute("flushInterval");
Integer size = context.getIntAttribute("size");
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, props);
}
}
2:子元素cache-ref解析
cacheRefElement方法负责解析cache-ref
元素,它通过调用CacheRefResolver的相应方法完成cache的引用
创建好的cache-ref
引用关系存入configuration的cacheRefMap缓存中
private void cacheRefElement(XNode context) {
if (context != null) {
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 通过调用resolveCacheRef完成cache的引用,将关系存储到Configuration中的cacheRefMap中
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
3:子元素resultMap解析
resultMapElement方法负责解析resultMap元素,它通过调用ResultMapResolver的相应方法完成resultMap的解析
创建好的resultMap存入configuration的resultMaps缓存中
该缓存以 namespace + resultMap
的 id 为 key,这里再次体现了 mybatis 的 namespace 的强大用处
/**
* 这是一个重载的方法,它接受一个 XNode 类型的参数 resultMapNode,代表 XML 中的 <resultMap> 元素。
* 方法内部调用了另一个重载版本的 resultMapElement 方法,并传入了一个空的 ResultMapping 列表作为额外的结果映射。
*/
private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}
/**
* 解析resultMap
* resultMapNode:代表 <resultMap> 元素的 XML 节点。
* additionalResultMappings:一个额外的结果映射列表,可能从其他地方继承而来。
*/
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
// 设置错误上下文,记录当前正在处理的 <resultMap> 元素的标识符。
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取 <resultMap> 元素的 id 属性,如果没有指定,则使用基于值的标识符。
String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
// 获取 <resultMap> 元素的 type 属性,如果没有指定,则依次尝试获取 ofType、resultType 和 javaType 属性。
String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType",resultMapNode.getStringAttribute("javaType"))));
// 获取 <resultMap> 元素的 extends 属性,用于指定继承的其他 <resultMap>。
String extend = resultMapNode.getStringAttribute("extends");
// 获取 <resultMap> 元素的 autoMapping 属性,指示是否启用自动映射
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 解析 type 属性中的类型字符串为 Java 类。
Class<?> typeClass = resolveClass(type);
// 初始化 discriminator 为 null,稍后可能会被赋值。
Discriminator discriminator = null;
// 创建一个新的 ResultMapping 列表,并添加任何额外的结果映射。
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
// 获取 <resultMap> 元素的所有子节点,并遍历它们。
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
// 如果子节点名称为 "constructor",则调用 processConstructorElement 方法来处理构造器元素。
// 如果子节点名称为 "discriminator",则调用 processDiscriminatorElement 方法来处理判别器元素,并设置 discriminator。
// 对于其他子节点,如果名称为 "id",则添加 ResultFlag.ID 标志,并构建一个 ResultMapping。
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
ArrayList<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 创建一个 ResultMapResolver 实例,用于解析和创建最终的 ResultMap。
// 尝试通过调用 resolve 方法来创建 ResultMap。
// 如果抛出 IncompleteElementException 异常,则将未完成的 ResultMapResolver 添加到配置中以供后续处理,并重新抛出异常。
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
4:子元素sql解析
sqlElement方法负责解析sql元素。
id属性用于区分不同的sql元素,在同一个mapper配置文件中可以配置多个sql元素
// 这段代码的主要作用是根据当前数据库 ID 来筛选并存储 SQL 片段,确保只有那些适用于当前环境的 SQL 片段会被加载和处理。
// 这样可以提高框架的灵活性和可扩展性,支持多种数据库类型的配置。
/**
* 这个方法接收一个 List<XNode> 类型的参数 list,其中 XNode 表示 XML 节点。
* 此方法的目的是根据当前数据库 ID 来选择性地处理 SQL 片段。
* 检查当前数据库 ID:
* 如果当前配置的 databaseId 不为空,则调用重载的 sqlElement 方法,并传入当前的 databaseId。
* 然后,再次调用重载的 sqlElement 方法,但这次传入 null 作为 requiredDatabaseId 参数,这意味着处理那些没有指定 databaseId 的 SQL 片段。
* 调用重载方法:
* 通过这种方式,确保了所有适用的 SQL 片段都会被正确处理。
*/
private void sqlElement(List<XNode> list) throws Exception {
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
/**
* 这个方法接收两个参数:
* list: 包含多个 XNode 的列表,每个 XNode 代表一个 SQL 片段。
* requiredDatabaseId: 一个字符串,表示当前处理的 SQL 片段必须匹配的数据库 ID。
* 遍历 SQL 片段: 遍历传入的 list 中的每个 XNode。
* 从每个 XNode 中提取 databaseId 和 id 属性。
* 使用 builderAssistant 应用命名空间到 id 上。
* 判断是否符合条件:
* 调用 databaseIdMatchesCurrent 方法来确定当前 SQL 片段是否应该被处理。
* 存储 SQL 片段:
* 如果符合条件,则将 SQL 片段存储在 sqlFragments 映射中,键为 id。
*/
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
String databaseId = context.getStringAttribute("databaseId");
String id = context.getStringAttribute("id");
id = builderAssistant.applyCurrentNamespace(id, false);
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) sqlFragments.put(id, context);
}
}
/*
* 这个方法用于判断给定的 SQL 片段是否应该被当前环境处理。
* 检查特定数据库 ID:
* 如果 requiredDatabaseId 不为空,则只有当 databaseId 与 requiredDatabaseId 相匹配时才返回 true。
* 处理通用 SQL 片段:
* 如果 requiredDatabaseId 为空,则检查 databaseId 是否也为空。
* 如果 databaseId 不为空,则直接返回 false。
* 接下来,检查 sqlFragments 映射中是否已经存在相同的 id,并且其 databaseId 是否不为空。如果是这种情况,则返回 false,因为已经有特定数据库 ID 的 SQL 片段存在。
* 返回结果:
* 如果以上条件都不满足,则返回 true,表示当前 SQL 片段可以被处理。
*/
private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
if (requiredDatabaseId != null) {
if (!requiredDatabaseId.equals(databaseId)) {
return false;
}
} else {
if (databaseId != null) {
return false;
}
// skip this fragment if there is a previous one with a not null databaseId
if (this.sqlFragments.containsKey(id)) {
XNode context = this.sqlFragments.get(id);
if (context.getStringAttribute("databaseId") != null) {
return false;
}
}
}
return true;
}
5:子元素statement解析
buildStatementFromContext()
方法负责解析statement元素。
id属性用于区分不同的statement元素,在同一个配置文件中可以配置多个statement元素。
通过调用XMLStatementBuilder#parseStatementNode()
方法完成解析。
在这个方法内有几个重要的步骤,理解他们对正确的配置statement元素很有帮助
5.1:动态解析子元素
statement节点可以配置各种子元素,比如前面提到的include子元素和selectKey子元素等(在动态sql里还有更多的子元素)。
动态解析子元素通过parseDynamicTags方法完成。
该方法根据子元素的类型递归的解析成一个个的SqlNode,这些SqlNode对象提供了apply方法,供后续调用时生成sql语句所需。
需要注意的是SelectKey没有对应的SqlNode对象,因为它的功能是用来生成KeyGenerator对象的(具体来说是SelectKeyGenerator对象)。
另外,SelectKey节点生成的KeyGenerator优先级高于statement节点的useGeneratedKeys属性生成的KeyGenerator对象
也就是说配置了SelectKey子节点就不需要再配置useGeneratedKeys属性了
5.2:生成SqlSource
SqlSource用于后续调用时根据SqlNode和参数对象生成sql语句。它接收一个叫做rootsqlNode的对象作为构造参数
5.3:生成KeyGenerator
如果配置了selectKey子元素,KeyGenerator直接使用selectKey子元素里生成的KeyGenerator对象(具体来说是SelectKeyGenerator对象)。
若没配置,则如果useGeneratedKeys属性的值为"true"且配置了 keyProperty属性,则生成默认的Jdbc3KeyGenerator对象
该对象调用JDBC驱动的getGeneratedKeys方法返回insert语句执行后生成的自增长主键
5.4:创建MapperStatement
MappedStatement对象封装了statement元素的所有属性以及子节点值,MappedStatement对象有一个id属性用于唯一标记它
这个id由namespace加statement元素的id属性值构成。
创建好的MappedStatement对象存入Configuration对象的mappedStatements缓存中,key为MappedStatement对象的id值
XMLMapperBuilder
// 这段代码的主要作用是根据当前数据库 ID 来筛选并解析 SQL 语句,确保只有那些适用于当前环境的 SQL 语句会被加载和处理。
// 这样可以提高框架的灵活性和可扩展性,支持多种数据库类型的配置。
// 如果解析过程中遇到不完整的元素,则将其标记为不完整并稍后处理,以避免程序中断。
/**
* 这个方法接收一个 List<XNode> 类型的参数 list,其中 XNode 表示 XML 节点。
* 此方法的目的是根据当前数据库 ID 来选择性地处理 SQL 语句。
* 检查当前数据库 ID:
* 如果当前配置的 databaseId 不为空,则调用重载的 buildStatementFromContext 方法,并传入当前的 databaseId。
* 然后,再次调用重载的 buildStatementFromContext 方法,但这次传入 null 作为 requiredDatabaseId 参数,这意味着处理那些没有指定 databaseId 的 SQL 语句。
* 调用重载方法:
* 通过这种方式,确保了所有适用的 SQL 语句都会被正确处理。
*/
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
buildStatementFromContext(list, configuration.getDatabaseId());
}
buildStatementFromContext(list, null);
}
/**
* 这个方法接收两个参数:
* list: 包含多个 XNode 的列表,每个 XNode 代表一个 SQL 语句。
* requiredDatabaseId: 一个字符串,表示当前处理的 SQL 语句必须匹配的数据库 ID。
* 遍历 SQL 语句:
* 遍历传入的 list 中的每个 XNode。
* 为每个 XNode 创建一个 XMLStatementBuilder 实例,用于解析 SQL 语句。
* 解析 SQL 语句:
* 调用 XMLStatementBuilder 的 parseStatementNode 方法来解析 SQL 语句。
* 如果在解析过程中遇到 IncompleteElementException 异常,则将 XMLStatementBuilder 实例添加到配置中的 incompleteStatements 集合中,以便稍后进行处理。
*/
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
XMLStatementBuilder
/**
* 解析 Statement 节点
*
* 该方法负责解析 MyBatis 配置文件中的 Statement 节点,如<select>、<insert>、<update>和<delete>节点
* 它提取节点的属性和内容,根据配置生成相应的 Statement 对象,并将其添加到 MyBatis 的 Configuration 中
*/
public void parseStatementNode() {
// 获取节点的 id 和 databaseId 属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 检查数据库是否匹配当前环境,如果不匹配则返回
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点名称并转换为 SqlCommandType 枚举
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 判断是否为 SELECT 语句
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 根据 flushCache 属性和是否 SELECT 语句设置 flushCache 值
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
// 根据 useCache 属性和是否 SELECT 语句设置 useCache 值
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
// 获取 resultOrdered 属性值
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// 在解析节点之前应用 Include 变换
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 解析参数类型
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取和解析语言驱动
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// 处理 selectKey 节点
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析 SQL 语句
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 根据 useGeneratedKeys 属性和语句类型选择合适的 KeyGenerator
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 创建 SqlSource 对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 解析 statement 类型
StatementType statementType = StatementType
.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 解析 fetchSize
Integer fetchSize = context.getIntAttribute("fetchSize");
// 解析 timeout
Integer timeout = context.getIntAttribute("timeout");
// 获取 parameterMap 属性
String parameterMap = context.getStringAttribute("parameterMap");
// 获取 resultType 属性并解析为类
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
// 获取 resultMap 属性
String resultMap = context.getStringAttribute("resultMap");
// 如果 resultType 和 resultMap 都未设置,则尝试从方法返回类型中解析 resultType
if (resultTypeClass == null && resultMap == null) {
resultTypeClass = MapperAnnotationBuilder.getMethodReturnType(builderAssistant.getCurrentNamespace(), id);
}
// 解析 resultSetType
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
// 获取 keyProperty 属性
String keyProperty = context.getStringAttribute("keyProperty");
// 获取 keyColumn 属性
String keyColumn = context.getStringAttribute("keyColumn");
// 获取 resultSets 属性
String resultSets = context.getStringAttribute("resultSets");
// 判断是否为 dirty select 语句
boolean dirtySelect = context.getBooleanAttribute("affectData", Boolean.FALSE);
// 添加映射语句到 Configuration 中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, fetchSize, timeout, parameterMap,
parameterTypeClass, resultMap, resultTypeClass, resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}
四:注册mapper类型
我们知道每个mapper配置文件的namespace属性对应于某个接口,应用程序通过接口访问mybatis时,mybatis会为这个接口生成一个代理对象,这个对象就叫mapper对象,在生成代理对象前mybatis会校验接口是否已注册,未注册的接口会产生一个异常。
为了避免这种异常,就需要注册mapper类型。这个步骤是在XMLMapperBuilder#bindMapperForNamespace()
方法中完成的。
它通过调用Configuration对象的addMapper方法完成,而Configuration对象的addMapper方法是通过MapperRegistry的addMapper方法完成的,它只是简单的将namespace属性对应的接口类型存入本地缓存中。
Configuration对象提供了一个重载的addMappers(StringpackageName)
方法,该方法以包路径名为参数,它的功能是自动扫描包路径下的接口并注册到MapperRegistry的缓存中,同时扫描包路径下的mapper配置文件并解析之。
解析配置文件是在MapperAnnotationBuilder类的parse方法里完成的,该方法先解析配置文件,然后再解析接口里的注解配置,且注解里的配置会覆盖配置文件里的配置,也就是说注解的优先级高于配置文件,这点需要注意。
采用自动扫描会大大简化配置,只不过需要应用程序自己调用,mybatis默认是不会调用这个方法的。
/**
* 为给定的命名空间绑定映射器类
* 如果在mybatis配置文件中使用了namespace属性,MyBatis会尝试通过该命名空间找到对应的类
* 这个方法的目的是尝试为这个命名空间绑定一个映射器类,如果这个类还没有被配置所识别
*/
private void bindMapperForNamespace() {
// 获取当前命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
// 尝试根据命名空间获取对应的类
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// 如果类不存在,忽略该异常,因为绑定类型不是必需的
}
// 如果找到了类,并且该类还没有在配置中被识别为映射器,则将其添加到配置中
if (boundType != null && !configuration.hasMapper(boundType)) {
// 标记该资源已加载,防止重复加载
configuration.addLoadedResource("namespace:" + namespace);
// 将映射器类添加到配置中
configuration.addMapper(boundType);
}
}
}