mybatis(三)配置mapper.xml 的基本操作


參考:https://www.cnblogs.com/wuzhenzhao/p/11101555.html

XML 映射文件

  本文參考mybatis中文官網進行學習總結:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html

  MyBatis 的真正強大在於它的映射語句,這是它的魔力所在。由於它的異常強大,映射器的 XML 文件就顯得相對簡單。如果拿它跟具有相同功能的 JDBC 代碼進行對比,你會立即發現省掉了將近 95% 的代碼。MyBatis 為聚焦於 SQL 而構建,以盡可能地為你減少麻煩。

  SQL 映射文件只有很少的幾個頂級元素(按照應被定義的順序列出):

  • cache – 對給定命名空間的緩存配置。
  • cache-ref – 對其他命名空間緩存配置的引用。
  • resultMap – 是最復雜也是最強大的元素,用來描述如何從數據庫結果集中來加載對象。
  • sql – 可被其他語句引用的可重用語句塊。
  • insert – 映射插入語句
  • update – 映射更新語句
  • delete – 映射刪除語句
  • select – 映射查詢語句

select

  查詢語句是 MyBatis 中最常用的元素之一,光能把數據存到數據庫中價值並不大,只有還能重新取出來才有用,多數應用也都是查詢比修改要頻繁。對每個插入、更新或刪除操作,通常間隔多個查詢操作。這是 MyBatis 的基本原則之一,也是將焦點和努力放在查詢和結果映射的原因。簡單查詢的 select 元素是非常簡單的。比如:

復制代碼
<resultMap id="BaseResultMap" type="blog">
     <id column="bid" property="bid" jdbcType="INTEGER"/>
     <result column="name" property="name" jdbcType="VARCHAR"/>
     <result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<!--****************************************************************************************-->
<!--簡單查詢-->
<select id="selectBlogById" resultMap="BaseResultMap" statementType="PREPARED" useCache="false">
     select * from blog where bid = #{bid}
</select>
復制代碼

  #{bid} 這就告訴 MyBatis 創建一個預處理語句(PreparedStatement)參數,在 JDBC 中,這樣的一個參數在 SQL 中會由一個“?”來標識,並被傳遞到一個新的預處理語句中,就像這樣:

// 近似的 JDBC 代碼,非 MyBatis 代碼...
String selectBlogById= "SELECT * FROM BLOG WHERE BID=?";
PreparedStatement ps = conn.prepareStatement(selectBlogById); ps.setInt(1,bid);

  當然,使用 JDBC 意味着需要更多的代碼來提取結果並將它們映射到對象實例中,而這就是 MyBatis 節省你時間的地方。select 元素允許你配置很多屬性來配置每條語句的作用細節。

復制代碼
<select
  id="selectPerson"
  parameterType="int"
  parameterMap="deprecated"
  resultType="hashmap"
  resultMap="personResultMap"
  flushCache="false"
  useCache="true"
  timeout="10"
  fetchSize="256"
  statementType="PREPARED"
  resultSetType="FORWARD_ONLY">
復制代碼

   相關屬性描述:

屬性 描述
id 在命名空間中唯一的標識符,可以被用來引用這條語句。
parameterType 將會傳入這條語句的參數類的完全限定名或別名。這個屬性是可選的,因為 MyBatis 可以通過類型處理器(TypeHandler) 推斷出具體傳入語句的參數,默認值為未設置(unset)。
parameterMap 這是引用外部 parameterMap 的已經被廢棄的方法。請使用內聯參數映射和 parameterType 屬性。
resultType 從這條語句中返回的期望類型的類的完全限定名或別名。 注意如果返回的是集合,那應該設置為集合包含的類型,而不是集合本身。可以使用 resultType 或 resultMap,但不能同時使用。
resultMap 外部 resultMap 的命名引用。結果集的映射是 MyBatis 最強大的特性,如果你對其理解透徹,許多復雜映射的情形都能迎刃而解。可以使用 resultMap 或 resultType,但不能同時使用。
flushCache 將其設置為 true 后,只要語句被調用,都會導致本地緩存和二級緩存被清空,默認值:false。
useCache 將其設置為 true 后,將會導致本條語句的結果被二級緩存緩存起來,默認值:對 select 元素為 true。
timeout 這個設置是在拋出異常之前,驅動程序等待數據庫返回請求結果的秒數。默認值為未設置(unset)(依賴驅動)。
fetchSize 這是一個給驅動的提示,嘗試讓驅動程序每次批量返回的結果行數和這個設置值相等。 默認值為未設置(unset)(依賴驅動)。
statementType STATEMENT,PREPARED 或 CALLABLE 中的一個。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED。
resultSetType FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等價於 unset) 中的一個,默認值為 unset (依賴驅動)。
databaseId 如果配置了數據庫廠商標識(databaseIdProvider),MyBatis 會加載所有的不帶 databaseId 或匹配當前 databaseId 的語句;如果帶或者不帶的語句都有,則不帶的會被忽略。
resultOrdered 這個設置僅針對嵌套結果 select 語句適用:如果為 true,就是假設包含了嵌套結果集或是分組,這樣的話當返回一個主結果行的時候,就不會發生有對前面結果集的引用的情況。 這就使得在獲取嵌套的結果集的時候不至於導致內存不夠用。默認值:false
resultSets 這個設置僅對多結果集的情況適用。它將列出語句執行后返回的結果集並給每個結果集一個名稱,名稱是逗號分隔的。

 insert, update 和 delete:

  數據變更語句 insert,update 和 delete 的實現非常接近:

復制代碼
<insert
  id="insertAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  keyProperty=""
  keyColumn=""
  useGeneratedKeys=""
  timeout="20">

<update
  id="updateAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">

<delete
  id="deleteAuthor"
  parameterType="domain.blog.Author"
  flushCache="true"
  statementType="PREPARED"
  timeout="20">
復制代碼

   相關屬性描述:

屬性 描述
id 命名空間中的唯一標識符,可被用來代表這條語句。
parameterType 將要傳入語句的參數的完全限定類名或別名。這個屬性是可選的,因為 MyBatis 可以通過類型處理器推斷出具體傳入語句的參數,默認值為未設置(unset)。
parameterMap 這是引用外部 parameterMap 的已經被廢棄的方法。請使用內聯參數映射和 parameterType 屬性。
flushCache 將其設置為 true 后,只要語句被調用,都會導致本地緩存和二級緩存被清空,默認值:true(對於 insert、update 和 delete 語句)。
timeout 這個設置是在拋出異常之前,驅動程序等待數據庫返回請求結果的秒數。默認值為未設置(unset)(依賴驅動)。
statementType STATEMENT,PREPARED 或 CALLABLE 的一個。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,默認值:PREPARED。
useGeneratedKeys (僅對 insert 和 update 有用)這會令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法來取出由數據庫內部生成的主鍵(比如:像 MySQL 和 SQL Server 這樣的關系數據庫管理系統的自動遞增字段),默認值:false。
keyProperty (僅對 insert 和 update 有用)唯一標記一個屬性,MyBatis 會通過 getGeneratedKeys 的返回值或者通過 insert 語句的 selectKey 子元素設置它的鍵值,默認值:未設置(unset)。如果希望得到多個生成的列,也可以是逗號分隔的屬性名稱列表。
keyColumn (僅對 insert 和 update 有用)通過生成的鍵值設置表中的列名,這個設置僅在某些數據庫(像 PostgreSQL)是必須的,當主鍵列不是表中的第一列的時候需要設置。如果希望使用多個生成的列,也可以設置為逗號分隔的屬性名稱列表。
databaseId 如果配置了數據庫廠商標識(databaseIdProvider),MyBatis 會加載所有的不帶 databaseId 或匹配當前 databaseId 的語句;如果帶或者不帶的語句都有,則不帶的會被忽略。

下面就是 insert,update 和 delete 語句的示例:

復制代碼
<insert id="insertAuthor">
  insert into Author (id,username,password,email,bio)
  values (#{id},#{username},#{password},#{email},#{bio})
</insert>

<update id="updateAuthor">
  update Author set
    username = #{username},
    password = #{password},
    email = #{email},
    bio = #{bio}
  where id = #{id}
</update>

<delete id="deleteAuthor">
  delete from Author where id = #{id}
</delete>
復制代碼

  如前所述,插入語句的配置規則更加豐富,在插入語句里面有一些額外的屬性和子元素用來處理主鍵的生成,而且有多種生成方式。首先,如果你的數據庫支持自動生成主鍵的字段(比如 MySQL 和 SQL Server),那么你可以設置 useGeneratedKeys=”true”,然后再把 keyProperty 設置到目標屬性上就 OK 了。如果你的數據庫還支持多行插入, 你也可以傳入一個 Blog數組或集合,並返回自動生成的主鍵。

復制代碼
<!--批量插入-->
<insert id="insertBlogs" useGeneratedKeys="true"
            keyProperty="bid">
     insert into blog (name, author_id) values
     <foreach item="item" collection="list" separator=",">
         (#{item.name}, #{item.authorId})
     </foreach>
</insert>
復制代碼

sql

  這個元素可以被用來定義可重用的 SQL 代碼段,這些 SQL 代碼可以被包含在其他語句中。它可以(在加載的時候)被靜態地設置參數。 在不同的包含語句中可以設置不同的值到參數占位符上。mybatis中sql標簽與include標簽進行配合,靈活的查詢需要的數據。

復制代碼
<sql id="ref">
    bid,name,authorId
</sql>

<select id="selectbyId" resultMap="BaseResultMap"> select <include refid="ref"/> from blog where bid = #{bid} </select>
復制代碼

  sql標簽中id屬性對應include標簽中的refid屬性。通過include標簽將sql片段和原sql片段進行拼接成一個完成的sql語句進行執行。include標簽中還可以用property標簽,用以指定自定義屬性。

復制代碼
<select id="selectbyId" resultMap="BaseResultMap">
    select
    <include refid="ref">
        <property name="abc" value="bid"/>
    </include>
    from
    blog where bid = #{bid}
</select>
復制代碼

  此時,可以在sql標簽中取出對應設置的自定義屬性中的值,例如接上代碼例子:

復制代碼
<sql id="ref">
    ${abc},name,authorId
</sql>

<select id="selectbyId" resultMap="BaseResultMap">
    select
    <include refid="ref">
        <property name="abc" value="bid"/>
    </include>
    from
    blog where bid = #{bid}
</select>
復制代碼

   在sql標簽中通過${}取出對應include標簽中設置的屬性值。

  關聯(association)元素處理“有一個”類型的關系。 比如,在我們的示例中,一個博客有一個用戶。關聯結果映射和其它類型的映射工作方式差不多。 你需要指定目標屬性名以及屬性的javaType(很多時候 MyBatis 可以自己推斷出來),在必要的情況下你還可以設置 JDBC 類型,如果你想覆蓋獲取結果值的過程,還可以設置類型處理器。關聯的不同之處是,你需要告訴 MyBatis 如何加載關聯。MyBatis 有兩種不同的方式加載關聯:

  • 嵌套 Select 查詢:通過執行另外一個 SQL 映射語句來加載期望的復雜類型。
  • 嵌套結果映射:使用嵌套的結果映射來處理連接結果的重復子集。

關聯的嵌套 Select 查詢

復制代碼
<!-- 另一種聯合查詢(一對一)的實現,但是這種方式有“N+1”的問題 -->
    <resultMap id="BlogWithAuthorQueryMap" type="com.wuzz.demo.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <association property="author" javaType="com.wuzz.demo.entity.Author"
                     column="author_id" select="selectAuthor"/>
    </resultMap>

    <!-- 嵌套查詢 -->
    <select id="selectAuthor" parameterType="int" resultType="com.wuzz.demo.entity.Author">
        select author_id authorId, author_name authorName
        from author where author_id = #{authorId}
    </select>

    <!-- 根據文章查詢作者,一對一,嵌套查詢,存在N+1問題,可通過開啟延遲加載解決 -->
    <select id="selectBlogWithAuthorQuery" resultMap="BlogWithAuthorQueryMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b
        left join author a
        on b.author_id=a.author_id
        where b.bid = #{bid, jdbcType=INTEGER}
    </select>
復制代碼

  就是這么簡單。我們有兩個 select 查詢語句:一個用來加載博客(Blog),另外一個用來加載作者(Author),而且博客的結果映射描述了應該使用 selectAuthor 語句加載它的 author 屬性。其它所有的屬性將會被自動加載,只要它們的列名和屬性名相匹配。這種方式雖然很簡單,但在大型數據集或大型數據表上表現不佳。這個問題被稱為“N+1 查詢問題”。 概括地講,N+1 查詢問題是這樣子的:

  • 你執行了一個單獨的 SQL 語句來獲取結果的一個列表(就是“+1”)。
  • 對列表返回的每條記錄,你執行一個 select 查詢語句來為每條記錄加載詳細信息(就是“N”)。

  這個問題會導致成百上千的 SQL 語句被執行。有時候,我們不希望產生這樣的后果。MyBatis 能夠對這樣的查詢進行延遲加載,因此可以將大量語句同時運行的開銷分散開來。mybatis.configuration.lazy-loading-enabled=true 可以開啟延時加載 mybatis.configuration.aggressive-lazy-loading=true 可以指定哪些方法調用查詢, 然而,如果你加載記錄列表之后立刻就遍歷列表以獲取嵌套的數據,就會觸發所有的延遲加載查詢,性能可能會變得很糟糕。所以還有另外一種方法。

關聯的嵌套結果映射

復制代碼
<!-- 根據文章查詢作者,一對一查詢的結果,嵌套查詢 -->
    <resultMap id="BlogWithAuthorResultMap" type="com.wuzz.demo.associate.BlogAndAuthor">
        <id column="bid" property="bid" jdbcType="INTEGER"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <!-- 聯合查詢,將author的屬性映射到ResultMap -->
        <association property="author" javaType="com.wuzz.demo.entity.Author">
            <id column="author_id" property="authorId"/>
            <result column="author_name" property="authorName"/>
        </association>
    </resultMap>
    <!-- 根據文章查詢作者,一對一,嵌套結果,無N+1問題 -->
    <select id="selectBlogWithAuthorResult" resultMap="BlogWithAuthorResultMap" >
        select b.bid, b.name, b.author_id, a.author_id , a.author_name
        from blog b,author a
        where b.author_id=a.author_id and b.bid = #{bid, jdbcType=INTEGER}
    </select>
復制代碼

   查詢文章帶評論的結果(一對多)映射:

復制代碼
<!--  查詢文章帶評論的結果(一對多) -->
    <resultMap id="BlogWithCommentMap" type="com.wuzz.demo.associate.BlogAndComment" extends="BaseResultMap" >
        <collection property="comment" ofType="com.wuzz.demo.entity.Comment">
            <id column="comment_id" property="commentId" />
            <result column="content" property="content" />
            <result column="bid" property="bid" />
        </collection>
    </resultMap>
    <!-- 根據文章查詢評論,一對多 -->
    <select id="selectBlogWithCommentById" resultMap="BlogWithCommentMap" >
        select b.bid, b.name, b.author_id , c.comment_id , c.content,c.bid
        from blog b, comment c
        where b.bid = c.bid
        and b.bid = #{bid}
    </select>
復制代碼

  按作者查詢文章評論的結果(多對多):

復制代碼
<!--  按作者查詢文章評論的結果(多對多) -->
    <resultMap id="AuthorWithBlogMap" type="com.wuzz.demo.associate.AuthorAndBlog" >
        <id column="author_id" property="authorId" jdbcType="INTEGER"/>
        <result column="author_name" property="authorName" jdbcType="VARCHAR"/>
        <collection property="blog" ofType="com.wuzz.demo.associate.BlogAndComment">
            <id column="bid" property="bid" />
            <result column="name" property="name" />
            <result column="author_id" property="authorId" />
            <collection property="comment" ofType="com.wuzz.demo.entity.Comment">
                <id column="comment_id" property="commentId" />
                <result column="content" property="content" />
                <result column="bid" property="bid" />
            </collection>
        </collection>
    </resultMap>

    <!-- 根據作者文章評論,多對多 -->
    <select id="selectAuthorWithBlog" resultMap="AuthorWithBlogMap" >
        select b.bid, b.name, a.author_id , a.author_name , c.comment_id , c.content,c.bid
        from blog b, author a, comment c
        where b.author_id = a.author_id and b.bid = c.bid
    </select>
復制代碼

動態 SQL

  MyBatis 的強大特性之一便是它的動態 SQL。如果你有使用 JDBC 或其它類似框架的經驗,你就能體會到根據不同條件拼接 SQL 語句的痛苦。例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最后一個列名的逗號。利用動態 SQL 這一特性可以徹底擺脫這種痛苦。雖然在以前使用動態 SQL 並非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射語句中的強大的動態 SQL 語言得以改進這種情形。動態 SQL 元素和 JSTL 或基於類似 XML 的文本處理器相似。在 MyBatis 之前的版本中,有很多元素需要花時間了解。MyBatis 3 大大精簡了元素種類,現在只需學習原來一半的元素便可。MyBatis 采用功能強大的基於 OGNL 的表達式來淘汰其它大部分元素。

  1. if
  2. choose (when, otherwise)
  3. trim (where, set)
  4. foreach

  其中  choose再實際開發中應用的較少,我們這里就其他3個標簽進行測試

復制代碼
<!--動態sql-->
    <select id="selectBlogById2" resultMap="BaseResultMap" statementType="PREPARED" useCache="false"
        parameterType="blog">
        select * from blog
        <trim prefix="WHERE" prefixOverrides="AND |OR ">
            <if test="bid != null and bid !='' ">
                bid = #{bid}
            </if>
            <if test="name != null and name !='' ">
                AND name = #{name}
            </if>
            <if test="authorId != null and authorId != ''">
                AND author_id = #{author_id}
            </if>
        </trim>
    </select>
復制代碼

   foreach:動態 SQL 的另外一個常用的操作需求是對一個集合進行遍歷,通常是在構建 IN 條件語句的時候。比如:

<select id="selectPostIn" resultType="domain.blog.Post">
  SELECT *
  FROM POST P
  WHERE ID in
  <foreach item="item" index="index" collection="list"
      open="(" separator="," close=")">
        #{item}
  </foreach>
</select>

  

批量操作
(spring-mybatis 工程單元測試目錄,MapperTest 類)
我們在生產的項目中會有一些批量操作的場景,比如導入文件批量處理數據的情況
(批量新增商戶、批量修改商戶信息),當數據量非常大,比如超過幾萬條的時候,在
Java 代碼中循環發送 SQL 到數據庫執行肯定是不現實的,因為這個意味着要跟數據庫創
建幾萬次會話,即使我們使用了數據庫連接池技術,對於數據庫服務器來說也是不堪重
負的。
在 MyBatis 里面是支持批量的操作的,包括批量的插入、更新、刪除。我們可以直
接傳入一個 List、Set、Map 或者數組,配合動態 SQL 的標簽,MyBatis 會自動幫我們
生成語法正確的 SQL 語句。
比如我們來看兩個例子,批量插入和批量更新。
批量插入
批量插入的語法是這樣的,只要在 values 后面增加插入的值就可以了。
insert into tbl_emp (emp_id, emp_name, gender,email, d_id) values ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) ,
( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? ) , ( ?,?,?,?,? )
 
在 Mapper 文件里面,我們使用 foreach 標簽拼接 values 部分的語句:
<!-- 批量插入 -->
<insert id="batchInsert" parameterType="java.util.List" useGeneratedKeys="true">
<selectKey resultType="long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
insert into tbl_emp (emp_id, emp_name, gender,email, d_id)
values
<foreach collection="list" item="emps" index="index"
separator=",">
( #{emps.empId},#{emps.empName},#{emps.gender},#{emps.email},#{emps.dId} )
</foreach>
</insert>
Java 代碼里面,直接傳入一個 List 類型的參數。
我們來測試一下。效率要比循環發送 SQL 執行要高得多。最關鍵的地方就在於減少
了跟數據庫交互的次數,並且避免了開啟和結束事務的時間消耗。
批量更新
批量更新的語法是這樣的,通過 case when,來匹配 id 相關的字段值。
update tbl_emp set
emp_name =
case emp_id
when ? then ?
when ? then ?
when ? then ? end ,
gender =
case emp_id
when ? then ?
when ? then ?
when ? then ? end ,
email =
case emp_id
when ? then ?
when ? then ?
when ? then ? end
where emp_id in ( ? , ? , ? )
所以在 Mapper 文件里面最關鍵的就是 case when 和 where 的配置。
需要注意一下 open 屬性和 separator 屬性。
<update id="updateBatch">
update tbl_emp set
emp_name =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id"
close="end">
when #{emps.empId} then #{emps.empName}
</foreach>
,gender =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id"
close="end">
when #{emps.empId} then #{emps.gender}
</foreach>
,email =
<foreach collection="list" item="emps" index="index" separator=" " open="case emp_id"
close="end">
when #{emps.empId} then #{emps.email}
</foreach>
where emp_id in
<foreach collection="list" item="emps" index="index" separator="," open="("
close=")">
#{emps.empId}
</foreach>
</update>
批量刪除也是類似的。
Batch Executor
當然 MyBatis 的動態標簽的批量操作也是存在一定的缺點的,比如數據量特別大的
時候,拼接出來的 SQL 語句過大。
MySQL 的服務端對於接收的數據包有大小限制,max_allowed_packet 默認是
4M,需要修改默認配置才可以解決這個問題。
Caused by: com.mysql.jdbc.PacketTooBigException: Packet for query is too large (7188967 >
4194304). You can change this value on the server by setting the max_allowed_packet' variable.
在我們的全局配置文件中,可以配置默認的 Executor 的類型。其中有一種
BatchExecutor。
 
<setting name="defaultExecutorType" value="BATCH" />
也可以在創建會話的時候指定執行器類型:
SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH);
BatchExecutor 底層是對 JDBC ps.addBatch()的封裝,原理是攢一批 SQL 以后再發
送(參考 standalone - 單元測試目錄 JdbcTest.java – testJdbcBatch())。
問題:三種執行器的區別是什么?Simple、Reuse、Batch
嵌套(關聯)查詢/ N+1 / 延遲加載
我們在查詢業務數據的時候經常會遇到跨表關聯查詢的情況,比如查詢員工就會關
聯部門(一對一),查詢成績就會關聯課程(一對一),查詢訂單就會關聯商品(一對
多),等等。

 

 

我們映射結果有兩個標簽,一個是 resultType,一個是 resultMap。
resultType 是 select 標簽的一個屬性,適用於返回 JDK 類型(比如 Integer、String
等等)和實體類。這種情況下結果集的列和實體類的屬性可以直接映射。如果返回的字
 
段無法直接映射,就要用 resultMap 來建立映射關系。
對於關聯查詢的這種情況,通常不能用 resultType 來映射。用 resultMap 映射,要
么就是修改 dto(Data Transfer Object),在里面增加字段,這個會導致增加很多無關
的字段。要么就是引用關聯的對象,比如 Blog 里面包含了一個 Author 對象,這種情況
下就要用到關聯查詢(association,或者嵌套查詢),MyBatis 可以幫我們自動做結果
的映射。
一對一的關聯查詢有兩種配置方式:
1、嵌套結果:
(mybatis-standalone - MyBatisTest - testSelectBlogWithAuthorResult ())
<!-- 根據文章查詢作者,一對一查詢的結果,嵌套查詢 -->
<resultMap id="BlogWithAuthorResultMap"
type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<!-- 聯合查詢,將 author 的屬性映射到 ResultMap -->
<association property="author" javaType="com.gupaoedu.domain.Author">
<id column="author_id" property="authorId"/>
<result column="author_name" property="authorName"/>
</association>
</resultMap>
2、嵌套查詢:
(mybatis-standalone - MyBatisTest - testSelectBlogWithAuthorQuery ())
<!-- 另一種聯合查詢 (一對一)的實現,但是這種方式有“N+1”的問題 -->
<resultMap id="BlogWithAuthorQueryMap" type="com.gupaoedu.domain.associate.BlogAndAuthor">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<association property="author" javaType="com.gupaoedu.domain.Author"
column="author_id" select="selectAuthor"/> <!-- selectAuthor 定義在下面-->
</resultMap>
<!-- 嵌套查詢 -->
 
<select id="selectAuthor" parameterType="int" resultType="com.gupaoedu.domain.Author">
select author_id authorId, author_name authorName
from author where author_id = #{authorId}
</select>
其中第二種方式:嵌套查詢,由於是分兩次查詢,當我們查詢了員工信息之后,會
再發送一條 SQL 到數據庫查詢部門信息。
我們只執行了一次查詢員工信息的 SQL(所謂的 1),如果返回了 N 條記錄,就會
再發送 N 條到數據庫查詢部門信息(所謂的 N),這個就是我們所說的 N+1 的問題。
這樣會白白地浪費我們的應用和數據庫的性能。
如果我們用了嵌套查詢的方式,怎么解決這個問題?能不能等到使用部門信息的時
候再去查詢?這個就是我們所說的延遲加載,或者叫懶加載。
在 MyBatis 里面可以通過開啟延遲加載的開關來解決這個問題。
在 settings 標簽里面可以配置:
<!--延遲加載的全局開關。當開啟時,所有關聯對象都會延遲加載。默認 false -->
<setting name="lazyLoadingEnabled" value="true"/>
<!--當開啟時,任何方法的調用都會加載該對象的所有屬性。默認 false,可通過 select 標簽的
fetchType 來覆蓋-->
<setting name="aggressiveLazyLoading" value="false"/>
<!-- Mybatis 創建具有延遲加載能力的對象所用到的代理工具,默認 JAVASSIST -->
<setting name="proxyFactory" value="CGLIB" />
lazyLoadingEnabled 決定了是否延遲加載。
aggressiveLazyLoading 決定了是不是對象的所有方法都會觸發查詢。
先來測試一下(也可以改成查詢列表):
1、沒有開啟延遲加載的開關,會連續發送兩次查詢;
2 、 開 啟 了 延 遲 加 載 的 開 關 , 調 用 blog.getAuthor() 以 及 默 認 的
(equals,clone,hashCode,toString)時才會發起第二次查詢,其他方法並不會觸發查
 
詢,比如 blog.getName();
3、如果開啟了 aggressiveLazyLoading=true,其他方法也會觸發查詢,比如
blog.getName()。
問題:為什么可以做到延遲加載?blog.getAuthor(),只是一個獲取屬性的方法,
里面並沒有連接數據庫的代碼,為什么會觸發對數據庫的查詢呢?
我懷疑:blog 根本不是 Blog 對象,而是被人動過了手腳!
把這個對象打印出來看看:
System.out.println(blog.getClass());
果然不對:
class com.gupaoedu.domain.associate.BlogAndAuthor_$$_jvst70_0
這個類的名字后面有 jvst,是 JAVASSIST 的縮寫。原來到這里帶延遲加載功能的對
象 blog 已經變成了一個代理對象,那到底什么時候變成代理對象的?我們后面在看源碼
的時候再去分析,這個也先留一個作業給大家。
【問題】當開啟了延遲加載的開關,對象是怎么變成代理對象的?
DefaultResultSetHandler.createResultObject()
既然是代理對象,那么必須要有一種創建代理對象的方法。我們有哪些實現動態代
理的方式?
這個就是為什么 settings 里面提供了一個 ProxyFactory 屬性。MyBatis 默認使用
JAVASSIST 創建代理對象。也可以改為 CGLIB,這時需要引入 CGLIB 的包。
【問題】CGLIB 和 JAVASSIST 區別是什么?
 
測試一下,我們把默認的 JAVASSIST 修改為 CGLIB,再打印這個對象。
【問題】
1、resultType 和 resultMap 的區別?
2、collection 和 association 的區別?
MBG 與 Example
https://github.com/mybatis/generator
我們在項目中使用 MyBaits 的時候,針對需要操作的一張表,需要創建實體類、
Mapper 映射器、Mapper 接口,里面又有很多的字段和方法的配置,這部分的工作是
非常繁瑣的。而大部分時候我們對於表的操作是相同的,比如根據主鍵查詢、根據 Map
查詢、單條插入、批量插入、根據主鍵刪除等等等等。當我們的表很多的時候,意味着
有大量的重復工作。所以有沒有一種辦法,可以根據我們的表,自動生成實體類、Mapper
映射器、Mapper 接口,里面包含了我們需要用到的這些基本方法和 SQL 呢?
給大家看一個類文件(DBToJavaVO.java),這個是我以前到一個公司的時候,項
目里面用的 Hibernate+Oracle,當時是我第一次用 Hibernate。我發現 PO 類和 VO 類
的格式基本上都是一樣的,屬性跟表字段一一對應,主要有幾個區別:
1、 類里面的屬性都是表里面的字段,然后定義 getter()、setter()方法;
2、 數據庫字段的下划線命名,要改成駝峰命名。
3、 PO 上面要加@Table、@Id、@Column 的注解;VO 不用;
4、 數據庫的常見類型,要改成 Java 類型,比如 varchar,要對應成 Java 的 String
類型。
當時我就靈機一動,能不能寫一個根據數據庫的表自動生成 PO 和 VO 類的工具呢?
 
這樣新建表的時候就不用一個一個寫字段,一個一個加注解了。於是我就寫了這個類。
后來我才知道有個東西叫 Hibernate 逆向工程。
MyBatis 也提供了一個這樣的東西,叫做 MyBatis Generator,簡稱 MBG。我們只
需要修改一個配置文件,使用相關的 jar 包命令或者 Java 代碼就可以幫助我們生成實體
類、映射器和接口文件。不知道用 MyBatis 的同學有沒有跟當年的我一樣,還是實體類
的一個一個字段,接口的一個一個方法,映射器的一條一條 SQL 去寫的。
MBG 的配置文件里面有一個 Example 的開關,這個東西用來構造復雜的篩選條件
的,換句話說就是根據我們的代碼去生成 where 條件(類似於 Tom 老師的自動生成
where 條件的方式)。
原理:在實體類中包含了兩個有繼承關系的 Criteria,用其中自動生成的方法來構建
查詢條件。把這個包含了 Criteria 的實體類作為參數傳到查詢參數中,在解析 Mapper
映射器的時候會轉換成 SQL 條件。
(mybatis-standalone 工程:
com.gupaoedu.domain.BlogExample
com.gupaoedu.BlogExampleTest)
BlogExample 里面包含了一個兩個 Criteria:
 
實例:查詢 bid=1 的 Blog,通過創建一個 Criteria 去構建查詢條件:
BlogMapper mapper = session.getMapper(BlogMapper.class);
BlogExample example = new BlogExample();
BlogExample.Criteria criteria = example.createCriteria();
criteria.andBidEqualTo(1);
List<Blog> list = mapper.selectByExample(example);
生成的語句:
select 'true' as QUERYID, bid, name, author_id from blog WHERE ( bid = ? )
翻頁
在寫存儲過程的年代,翻頁也是一件很難調試的事情,我們要實現數據不多不少准
確地返回,需要大量的調試和修改。但是如果自己手寫過分頁,就能清楚分頁的原理。
邏輯翻頁與物理翻頁
在我們查詢數據庫的操作中,有兩種翻頁方式,一種是邏輯翻頁(假分頁),一種
是物理翻頁(真分頁)。邏輯翻頁的原理是把所有數據查出來,在內存中刪選數據。 物
理翻頁是真正的翻頁,比如 MySQL 使用 limit 語句,Oracle 使用 rownum 語句,SQL
Server 使用 top 語句。
邏輯翻頁
MyBatis 里面有一個邏輯分頁對象 RowBounds,里面主要有兩個屬性,offset 和
limit(從第幾條開始,查詢多少條)。
我們可以在 Mapper 接口的方法上加上這個參數,不需要修改 xml 里面的 SQL 語句。
public List<Blog> selectBlogList(RowBounds rowBounds);
使用:mybatis-standalone- MyBatisTest-testSelectByRowBounds()
 
int start = 10; // offset,從第幾行開始查詢
int pageSize = 5; // limit,查詢多少條
RowBounds rb = new RowBounds(start, pageSize);
List<Blog> list = mapper.selectBlogList(rb);
for(Blog b :list){
System.out.println(b);
}
它的底層其實是對 ResultSet 的處理。它會舍棄掉前面 offset 條數據,然后再取剩
下的數據的 limit 條。
// DefaultResultSetHandler.java
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap,
ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws
SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext();
ResultSet resultSet = rsw.getResultSet();
this.skipRows(resultSet, rowBounds);
while(this.shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() &&
resultSet.next()) {
ResultMap discriminatedResultMap = this.resolveDiscriminatedResultMap(resultSet,
resultMap, (String)null);
Object rowValue = this.getRowValue(rsw, discriminatedResultMap, (String)null);
this.storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
}
}
很明顯,如果數據量大的話,這種翻頁方式效率會很低(跟查詢到內存中再使用
subList(start,end)沒什么區別)。所以我們要用到物理翻頁。
物理翻頁
物理翻頁是真正的翻頁,它是通過數據庫支持的語句來翻頁。
第一種簡單的辦法就是傳入參數(或者包裝一個 page 對象),在 SQL 語句中翻頁。
<select id="selectBlogPage" parameterType="map" resultMap="BaseResultMap">
 
select * from blog limit #{curIndex} , #{pageSize}
</select>
第一個問題是我們要在 Java 代碼里面去計算起止序號;第二個問題是:每個需要翻
頁的 Statement 都要編寫 limit 語句,會造成 Mapper 映射器里面很多代碼冗余。
那我們就需要一種通用的方式,不需要去修改配置的任何一條 SQL 語句,只要在我
們需要翻頁的地方封裝一下翻頁對象就可以了。
我們最常用的做法就是使用翻頁的插件,這個是基於 MyBatis 的攔截器實現的,比
如 PageHelper。
// pageSize 每一頁幾條
PageHelper.startPage(pn, 10);
List<Employee> emps = employeeService.getAll();
// navigatePages 導航頁碼數
PageInfo page = new PageInfo(emps, 10);
return Msg.success().add("pageInfo", page);
PageHelper 是通過 MyBatis 的攔截器實現的,插件的具體原理我們后面的課再分
析。簡單地來說,它會根據 PageHelper 的參數,改寫我們的 SQL 語句。比如 MySQL
會生成 limit 語句,Oracle 會生成 rownum 語句,SQL Server 會生成 top 語句。
通用 Mapper
問題:當我們的表字段發生變化的時候,我們需要修改實體類和 Mapper 文件定義
的字段和方法。如果是增量維護,那么一個個文件去修改。如果是全量替換,我們還要
去對比用 MBG 生成的文件。字段變動一次就要修改一次,維護起來非常麻煩。
解決這個問題,我們有兩種思路。
第 一 個 , 因 為 MyBatis 的 Mapper 是 支 持 繼 承 的 ( 見 :
 
https://github.com/mybatis/mybatis-3/issues/35 ) 。 所 以 我 們 可 以 把 我 們 的
Mapper.xml 和 Mapper 接口都分成兩個文件。一個是 MBG 生成的,這部分是固定不變
的。然后創建 DAO 類繼承生成的接口,變化的部分就在 DAO 里面維護。
mybatis-standalone 工程:
public interface BlogMapperExt extends BlogMapper {
public Blog selectBlogByName(String name);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gupaoedu.mapper.BlogMapperExt">
<!-- 只能繼承 statement,不能繼承 sql、resultMap 等標簽 -->
<resultMap id="BaseResultMap" type="com.gupaoedu.domain.Blog">
<id column="bid" property="bid" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="author_id" property="authorId" jdbcType="INTEGER"/>
</resultMap>
<!-- 在 parent xml 和 child xml 的 statement id 相同的情況下,會使用 child xml 的 statement
id -->
<select id="selectBlogByName" resultMap="BaseResultMap" statementType="PREPARED">
select * from blog where name = #{name}
</select>
</mapper>
mybatis-config.xml 里面也要掃描:
<mappers>
<mapper resource="BlogMapper.xml"/>
<mapper resource="BlogMapperExt.xml"/>
</mappers>
所以以后只要修改 Ext 的文件就可以了。這么做有一個缺點,就是文件會增多。
思考:既然針對每張表生成的基本方法都是一樣的,也就是公共的方法部分代碼都
是一樣的,我們能不能把這部分合並成一個文件,讓它支持泛型呢?
 
太聰明了,當然可以!
編寫一個支持泛型的通用接口,比如叫 GPBaseMapper<T>,把實體類作為參數傳
入。這個接口里面定義了大量的增刪改查的基礎方法,這些方法都是支持泛型的。
自 定 義 的 Mapper 接 口 繼 承 該 通 用 接 口 , 例 如 BlogMapper extends
GPBaseMapper<Blog>,自動獲得對實體類的操作方法。遇到沒有的方法,我們依然
可以在我們自己的 Mapper 里面編寫。
我們能想到的解決方案,早就有人做了這個事了,這個東西就叫做通用 Mapper。
https://github.com/abel533/Mapper/wiki
用途:主要解決單表的增刪改查問題,並不適用於多表關聯查詢的場景。
除了配置文件變動的問題之外,通用 Mapper 還可以解決:
1、 每個 Mapper 接口中大量的重復方法的定義;
2、 屏蔽數據庫的差異;
3、 提供批量操作的方法;
4、 實現分頁。
通用 Mapper 和 PageHelper 作者是同一個人(劉增輝)。
使用方式:在 Spring 中使用時,引入 jar 包,替換 applicationContext.xml 中的
sqlSessionFactory 和 configure。
<bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.gupaoedu.crud.dao"/>
</bean>
 
MyBatis-Plus
https://mybatis.plus/guide
MyBatis-Plus 是原生 MyBatis 的一個增強工具,可以在使用原生 MyBatis 的所有
功能的基礎上,使用 plus 特有的功能。
MyBatis-Plus 的核心功能:
通用 CRUD:定義好 Mapper 接口后,只需要繼承 BaseMapper<T> 接口即
可獲得通用的增刪改查功能,無需編寫任何接口方法與配置文件。
條件構造器:通過 EntityWrapper<T>(實體包裝類),可以用於拼接 SQL 語
句,並且支持排序、分組查詢等復雜的 SQL。 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM