Mybatis源码05 - Mapper映射文件的配置

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);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值