天天看點

【深入淺出MyBatis系列三】Mapper映射檔案配置

#0 系列目錄#

  • 深入淺出MyBatis系列
  • 【深入淺出MyBatis系列一】MyBatis入門
  • 【深入淺出MyBatis系列二】配置簡介(MyBatis源碼篇)
  • 【深入淺出MyBatis系列三】Mapper映射檔案配置
  • 【深入淺出MyBatis系列四】強大的動态SQL
  • 【深入淺出MyBatis系列五】SQL執行流程分析(源碼篇)
  • 【深入淺出MyBatis系列六】插件原理
  • 【深入淺出MyBatis系列七】分頁插件
  • 【深入淺出MyBatis系列八】SQL自動生成插件
  • 【深入淺出MyBatis系列九】改造Cache插件
  • 【深入淺出MyBatis系列十】與Spring內建
  • 【深入淺出MyBatis系列十一】緩存源碼分析
  • 【深入淺出MyBatis系列十二】終結篇:MyBatis原理深入解析

在mapper檔案中,以mapper作為根節點,其下面可以配置的元素節點有: select, insert, update, delete, cache, cache-ref, resultMap, sql 。

#1 insert, update, delete 的配置及使用# 相信,看到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- 配置本定命名空間的緩存。
        type- cache實作類,預設為PERPETUAL,可以使用自定義的cache實作類(别名或完整類名皆可)
        eviction- 回收算法,預設為LRU,可選的算法有:
            LRU– 最近最少使用的:移除最長時間不被使用的對象。
            FIFO– 先進先出:按對象進入緩存的順序來移除它們。
            SOFT– 軟引用:移除基于垃圾回收器狀态和軟引用規則的對象。
            WEAK– 弱引用:更積極地移除基于垃圾收集器狀态和弱引用規則的對象。
        flushInterval- 重新整理間隔,預設為1個小時,機關毫秒
        size- 緩存大小,預設大小1024,機關為引用數
        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
      <!-- 1. id (必須配置)
        id是命名空間中的唯一辨別符,可被用來代表這條語句。 
        一個命名空間(namespace) 對應一個dao接口, 
        這個id也應該對應dao裡面的某個方法(相當于方法的實作),是以id 應該與方法名一緻 -->
      
      id="insertUser"
      
      <!-- 2. parameterType (可選配置, 預設為mybatis自動選擇處理)
        将要傳入語句的參數的完全限定類名或别名, 如果不配置,mybatis會通過ParameterHandler 根據參數類型預設選擇合适的typeHandler進行處理
        parameterType 主要指定參數類型,可以是int, short, long, string等類型,也可以是複雜類型(如對象) -->
      
      parameterType="com.demo.User"
      
      <!-- 3. flushCache (可選配置,預設配置為true)
        将其設定為 true,任何時候隻要語句被調用,都會導緻本地緩存和二級緩存都會被清空,預設值:true(對應插入、更新和删除語句) -->
      
      flushCache="true"
      
      <!-- 4. statementType (可選配置,預設配置為PREPARED)
        STATEMENT,PREPARED 或 CALLABLE 的一個。這會讓 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,預設值:PREPARED。 -->
      
      statementType="PREPARED"
      
      <!-- 5. keyProperty (可選配置, 預設為unset)
        (僅對 insert 和 update 有用)唯一标記一個屬性,MyBatis 會通過 getGeneratedKeys 的傳回值或者通過 insert 語句的 selectKey 子元素設定它的鍵值,預設:unset。如果希望得到多個生成的列,也可以是逗号分隔的屬性名稱清單。 -->
      
      keyProperty=""
      
      <!-- 6. keyColumn     (可選配置)
        (僅對 insert 和 update 有用)通過生成的鍵值設定表中的列名,這個設定僅在某些資料庫(像 PostgreSQL)是必須的,當主鍵列不是表中的第一列的時候需要設定。如果希望得到多個生成的列,也可以是逗号分隔的屬性名稱清單。 -->
      
      keyColumn=""
      
      <!-- 7. useGeneratedKeys (可選配置, 預設為false)
        (僅對 insert 和 update 有用)這會令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法來取出由資料庫内部生成的主鍵(比如:像 MySQL 和 SQL Server 這樣的關系資料庫管理系統的自動遞增字段),預設值:false。  -->
      
      useGeneratedKeys="false"
      
      <!-- 8. timeout  (可選配置, 預設為unset, 依賴驅動)
        這個設定是在抛出異常之前,驅動程式等待資料庫傳回請求結果的秒數。預設值為 unset(依賴驅動)。 -->
      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>
           

以上就是一個模闆配置, 哪些是必要配置,哪些是根據自己實際需求,看一眼就知道了。看一個真實的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(别名), 那麼這個地方,用上别名,豈不是技能跟蛋疼的長長的包名說拜拜了

。好啦,咱們配上别名,在哪兒配? 當然是在mybatis 的全局配置檔案(我這兒名字是mybatis-conf.xml), 不要認為是在mapper的配置檔案裡面配置哈。

<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> 這個元素節點吧:**selectKey給了你一個簡單的行為在你的資料庫中來處理自動生成的主鍵,而不需要使你的Java代碼變得複雜。在上面的示例中,selectKey元素将會首先運作,userid會被設定,然後插入語句會被調用。

另外,selectKey節點生成的KeyGenerator優先級高于statement節點的useGeneratedKeys屬性生成的KeyGenerator對象,也就是說配置了SelectKey子節點就不需要再配置useGeneratedKeys屬性了

<selectKey
        <!-- selectKey 語句結果應該被設定的目标屬性。如果希望得到多個生成的列,也可以是逗号分隔的屬性名稱清單。 -->
        keyProperty="id"
        <!-- 結果的類型。MyBatis 通常可以推算出來,但是為了更加确定寫上也不會有什麼問題。MyBatis 允許任何簡單類型用作主鍵的類型,包括字元串。如果希望作用于多個生成的列,則可以使用一個包含期望屬性的 Object 或一個 Map。 -->
        resultType="int"
        <!-- 這可以被設定為 BEFORE 或 AFTER。如果設定為 BEFORE,那麼它會首先選擇主鍵,設定 keyProperty 然後執行插入語句。如果設定為 AFTER,那麼先執行插入語句,然後是 selectKey 元素 - 這和像 Oracle 的資料庫相似,在插入語句内部可能有嵌入索引調用。 -->
        order="BEFORE"
        <!-- 與前面相同,MyBatis 支援 STATEMENT,PREPARED 和 CALLABLE 語句的映射類型,分别代表 PreparedStatement 和 CallableStatement 類型。 -->
        statementType="PREPARED">
           

#2 select、resultMap的配置及使用## select無疑是我們最常用,也是最複雜的,mybatis通過resultMap能幫助我們很好地進行進階映射。下面就開始看看select 以及 resultMap的用法:

先看select的配置吧:

<select
     <!--  1. id (必須配置)
        id是命名空間中的唯一辨別符,可被用來代表這條語句。 
        一個命名空間(namespace) 對應一個dao接口, 
        這個id也應該對應dao裡面的某個方法(相當于方法的實作),是以id 應該與方法名一緻
     -->
     
     id="selectPerson"
     
     <!-- 2. parameterType (可選配置, 預設為mybatis自動選擇處理)
        将要傳入語句的參數的完全限定類名或别名, 如果不配置,mybatis會通過ParameterHandler 根據參數類型預設選擇合适的typeHandler進行處理
        parameterType 主要指定參數類型,可以是int, short, long, string等類型,也可以是複雜類型(如對象) -->
     parameterType="int"
     
     <!-- 3. resultType (resultType 與 resultMap 二選一配置)
         resultType用以指定傳回類型,指定的類型可以是基本類型,可以是java容器,也可以是javabean -->
     resultType="hashmap"
     
     <!-- 4. resultMap (resultType 與 resultMap 二選一配置)
         resultMap用于引用我們通過 resultMap标簽定義的映射類型,這也是mybatis元件進階複雜映射的關鍵 -->
     resultMap="personResultMap"
     
     <!-- 5. flushCache (可選配置)
         将其設定為 true,任何時候隻要語句被調用,都會導緻本地緩存和二級緩存都會被清空,預設值:false -->
     flushCache="false"
     
     <!-- 6. useCache (可選配置)
         将其設定為 true,将會導緻本條語句的結果被二級緩存,預設值:對 select 元素為 true -->
     useCache="true"
     
     <!-- 7. timeout (可選配置) 
         這個設定是在抛出異常之前,驅動程式等待資料庫傳回請求結果的秒數。預設值為 unset(依賴驅動)-->
     timeout="10000"
     
     <!-- 8. fetchSize (可選配置) 
         這是嘗試影響驅動程式每次批量傳回的結果行數和這個設定值相等。預設值為 unset(依賴驅動)-->
     fetchSize="256"
     
     <!-- 9. statementType (可選配置) 
         STATEMENT,PREPARED 或 CALLABLE 的一個。這會讓 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,預設值:PREPARED-->
     statementType="PREPARED"
     
     <!-- 10. resultSetType (可選配置) 
         FORWARD_ONLY,SCROLL_SENSITIVE 或 SCROLL_INSENSITIVE 中的一個,預設值為 unset (依賴驅動)-->
     resultSetType="FORWARD_ONLY">
           

配置看起來總是這麼多,不過實際常用的配置也就那麼幾個, 根據自己的需要吧,上面都已注明是否必須配置。看一個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>
           

上面的示例,我們針對course, 簡單示範了 select的用法, 不過有個問題值得思考:

一個student可以對應多個course, 那麼,在mybatis中如何處理這種一對多, 甚至于多對多,一對一的關系呢?

這兒,就不得不提到 resultMap 這個東西,

mybatis的resultMap功能可謂十分強大,能夠處理複雜的關系映射

, 那麼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實體類);對應的資料庫表的列名稱;

            collection,對應javabean中容器類型, 是實作一對多的關鍵 
            property 為javabean中容器對應字段名
            column 為展現在資料庫中列名
            ofType 就是指定javabean中容器指定的類型

            不同情況需要告訴MyBatis 如何加載一個聚集。MyBatis 可以用兩種方式加載:
                1. select: 執行一個其它映射的SQL 語句傳回一個Java實體類型。較靈活;
                2. resultMap: 使用一個嵌套的結果映射來處理通過join查詢結果集,映射成Java實體類型。
        -->
        <collection property="" column="" ofType=""></collection>
        
        <!-- 
            聯合元素用來處理“一對一”的關系。需要指定映射的Java實體類的屬性,屬性的javaType(通常MyBatis 自己會識别)。對應的資料庫表的列名稱。如果想覆寫的話傳回結果的值,需要指定typeHandler。

            association 為關聯關系,是實作N對一的關鍵。
            property 為javabean中容器對應字段名
            column 為展現在資料庫中列名
            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>
           

好啦,知道resutMap怎麼配置後,咱們立即接着上面的demo來練習一下吧,

一個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>
           

當然,我們需要定義StudentEntity實體類的構造方法:

public StudentEntity(String studentID, String studentName, String studentSex, Date studentBirthday){  
    this.studentID = studentID;  
    this.studentName = studentName;  
    this.studentSex = studentSex;  
    this.studentBirthday = studentBirthday;  
}
           

相信通過以上示例, 大家也能夠使用mybatis的select 和 resultMap的用法了。上面隻示範了一對多的映射,其實多對一、多對多也與它類似,是以我就沒示範了,有興趣的可以自己動手再做做。

#3 字元串代入法# 預設的情況下,

使用#{}文法會促使MyBatis 生成PreparedStatement 屬性并且使用PreparedStatement 的參數(=?)來安全的設定值

。盡量這些是快捷安全,也是經常使用的。但有時候你可能想直接未更改的字元串代入到SQL 語句中。比如說,

對于ORDER BY,你可能會這樣使用:ORDER BY ${columnName}但MyBatis 不會修改和規避掉這個字元串

注意:這樣地接收和應用一個使用者輸入到未更改的語句中,是非常不安全的。這會讓使用者能植入破壞代碼,是以,要麼要求字段不要允許客戶輸入,要麼你直接來檢測他的合法性 。

#4 子元素之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) {
      String type = context.getStringAttribute("type", "PERPETUAL");
      Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
      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);
    }
  }
           

#5 子元素之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 {
    	  cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
    	  configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }
           

#6 子元素之resultMap解析# resultMapElement方法負責解析resultMap元素,它通過調用ResultMapResolver的相應方法完成resultMap的解析。建立好的resultMap存入configuration的resultMaps緩存中(該緩存以namespace+resultMap的id為key,這裡再次展現了mybatis的namespace的強大用處)。

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
    return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
  }

  private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    String id = resultMapNode.getStringAttribute("id",
        resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
        resultMapNode.getStringAttribute("ofType",
            resultMapNode.getStringAttribute("resultType",
                resultMapNode.getStringAttribute("javaType"))));
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    Class<?> typeClass = resolveClass(type);
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
    resultMappings.addAll(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
      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 resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
      return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
      configuration.addIncompleteResultMap(resultMapResolver);
      throw e;
    }
  }
           

#7 子元素之sql解析# sqlElement方法負責解析sql元素。id屬性用于區分不同的sql元素,在同一個mapper配置檔案中可以配置多個sql元素。

private void sqlElement(List<XNode> list) throws Exception {
    if (configuration.getDatabaseId() != null) {
      sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
  }

  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);
    }
  }
  
  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;
  }
           

#8 子元素之statement解析# buildStatementFromContext方法負責解析statement元素。id屬性用于區分不同的statement元素,在同一個配置檔案中可以配置多個statement元素。

通過調用XMLStatementBuilder的parseStatementNode方法完成解析

。在這個方法内有幾個重要的步驟,了解他們對正确的配置statement元素很有幫助。

  1. 動态解析子元素

statement節點可以配置各種子元素,比如前面提到的include子元素和selectKey子元素等(在動态sql裡還有更多的子元素,具體參考mybatis的官方文檔)。動态解析子元素通過parseDynamicTags方法完成。該方法根據子元素的類型遞歸的解析成一個個的SqlNode,這些SqlNode對象提供了apply方法,供後續調用時生成sql語句所需。需要注意的是SelectKey沒有對應的SqlNode對象,因為它的功能是用來生成KeyGenerator對象的(具體來說是SelectKeyGenerator對象)。另外,SelectKey節點生成的KeyGenerator優先級高于statement節點的useGeneratedKeys屬性生成的KeyGenerator對象,也就是說配置了SelectKey子節點就不需要再配置useGeneratedKeys屬性了。

  1. 生成SqlSource

SqlSource用于後續調用時根據SqlNode和參數對象生成sql語句。它接收一個叫做rootsqlNode的對象作為構造參數。

  1. 生成KeyGenerator

如果配置了selectKey子元素,KeyGenerator直接使用selectKey子元素裡生成的KeyGenerator對象(具體來說是SelectKeyGenerator對象)。若沒配置,則如果useGeneratedKeys屬性的值為"true"且配置了 keyProperty屬性,則生成預設的Jdbc3KeyGenerator對象,該對象調用JDBC驅動的getGeneratedKeys方法傳回insert語句執行後生成的自增長主鍵。

  1. 建立MappedStatement

MappedStatement對象封裝了statement元素的所有屬性以及子節點值,MappedStatement對象有一個id屬性用于唯一标記它,這個id由namespace加statement元素的id屬性值構成。建立好的MappedStatement對象存入Configuration對象的mappedStatements緩存中,key為MappedStatement對象的id值。

XMLMapperBuilder.java:

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  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.java:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) return;

    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    Class<?> resultTypeClass = resolveClass(resultType);
    String resultSetType = context.getStringAttribute("resultSetType");
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);
    
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    String resultSets = context.getStringAttribute("resultSets");
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? new Jdbc3KeyGenerator() : new NoKeyGenerator();
    }

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }
           

#9 注冊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預設是不會調用這個方法的(後續将會講解的spring內建mybatis就用到了自動掃描)

private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          configuration.addMapper(boundType);
        }
      }
    }
  }
           

版權聲明:本文為CSDN部落客「weixin_34208283」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/weixin_34208283/article/details/91531952