首頁>技術>

前言

上節我們介紹了 《 Mybatis系列全解(四):全網最全!Mybatis配置檔案 XML 全貌詳解 》,內容很詳細( 也很枯燥),由於篇幅實在過於冗長,我預計大家想看完得花上兩段上班地鐵公交車的時間 。。。

不過應該有讓大家瞭解到 Mybatis 的核心配置檔案 config.xml 全貌,其中的 元素即是我們本節準備登場介紹的 SQL 對映器,上節有介紹了三種引入 SQL 對映器的方式,本節我們就主要聊聊它的幾個頂級元素用法。

Mybatis 真正強大就在於它的語句對映,這是它的魔力所在,也是基石。由於它異常強大,對映器的 XML 檔案就顯得相對簡單。如果拿它跟具有相同功能的 JDBC 程式碼進行對比,你會立即發現省掉了將近 95% 的程式碼( 95% 是Mybatis 官網的說法 ,我也就引入一下 ),MyBatis 致力於減少使用成本,讓使用者能更專注於 SQL 程式碼。

一、mapper 對映器頂級元素全貌

與其它 ORM 框架如 Hibernate 不同,Mybatis 的框架思想希望開發者能夠直接操作資料庫編寫 SQL,而不是隱藏起來,讓開發者獨自面對 Java 物件,為此 Mybatis 設計了 SQL 對映器,任你五招十二式。

對映器有九大頂級元素 ,基本技能介紹

select : 用於查詢,支援傳參,返回指定結果集;insert : 用於新增,支援傳參,返回指定結果集;update : 用於更新,支援傳參,返回指定結果集;delete : 用於刪除,支援傳參,返回指定結果集;sql : 被其它語句引用的 可複用 語句塊;cache : 當前名稱空間快取配置;cache-ref : 引用其它名稱空間的快取配置;parameterMap : 引數對映,已棄用,是它不夠好;resultMap : 結果集對映,它就很好;

其中,增刪改查操作拼接 SQL 時使用到的 動態SQL( if、where、foreach啥的),以及封裝結果集時使用到的 複雜對映 (1對1 ,1對多,多對多啥的),這兩部分我們後面單立文章再詳細介紹,本文中我們簡單點過。

九大頂級元素 ,功能歸類:

其中頂一元素 parameterMap 已建議棄用了 。

無論你有多麼複雜的 SQL 操作,最根本的思路都逃不出以上 4 部分。

二、namespace 名稱空間

一個完整的 Mapper 對映檔案,需要有約束頭 xml 與 !DOCTYPE ,其次才是 mapper 根元素,最後再是頂級元素,而其中,namespace 屬性作為 mapper 的唯一標識,試回憶:

上學時,6年級一班23號,能代表唯一的你。編寫 Java 類時,包名 + 類名,能代表唯一的類。而如今,我們在 Mybatis 中寫的每一段 SQL 語句,同樣有唯一的代表方式,那就是「 名稱空間標識 + 語句id 」,無論是為了區分業務也好,還是為了拆分服務也好,反正 Mybatis 讓每一個 mapper.xml 配備一個唯一名稱空間標識。

每一段 SQL 語句都是唯一定義的,我們在 Mybatis 中用「 名稱空間標識 + 語句塊 ID 」作為唯一的標識,組合之後在 Mybatis 二級快取中可以作為本地 map 集合 快取 的唯一Key ,也可以用於 Dao 介面的 對映 繫結,還能作為唯一 代理 標識。總之,我們希望避免命名衝突和重複定義,所以,擁有這麼一個唯一標識 ,它就至少有一億個利好。

三、select 查詢

select 查詢語句,幾乎是我們最高頻的使用元素,所以 Mybatis 在這塊沒少下功夫,目的就是透過提供儘可能多的便利,讓我們的查詢操作變得簡單。 一個查詢使用者 User 的查詢語句可以這麼編寫:

<select id="selectUser" parameterType="int" resultType="hashmap">  select * from t_user where id = #{id}</select>
id屬性:在當前 mapper.xml 名稱空間下,它的 id 值是唯一的( 不過如果在不同的 mapper.xml 名稱空間下,則允許有相同的的 id 值 )parameterType 屬性:代表傳入的引數型別,這裡是 int (或 Integer)型別resultType屬性:代表返回結果型別,這裡指定返回一個 hashMap 型別的物件,mybatis 會把查詢出來的資料表記錄對應的 ' 欄位列名 - 欄位值 ',自動對映為 map 集合的 key - value

當然如果你不希望透過 hashmap 來接收查詢結果,允許你自由指定返回型別。Mybatis 是支援自動繫結 JavaBean 的,我們只要讓查詢返回的欄位名和 JavaBean 的屬性名保持一致(或者採用駝峰式命名),便可以自動對映結果集,例如你建立一個 Java 類 User.java ,包含兩個屬性 id 和 name , 那麼結果集可以指定為 com.vo.User ,就完成了。

<select id="selectUser"     parameterType="int" resultType="com.vo.User">  select * from t_user where id = #{id}</select>

注意引數符號:

#{id}

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

// 近似的 JDBC 程式碼,非 MyBatis 程式碼...String selectUser = " select * from t_user where id = ? ";PreparedStatement ps = conn.prepareStatement(selectUser);ps.setInt(1,id);

#{} 作為佔位符,${} 作為替換符,兩者沒有孰輕孰重,只不過應用場景不同,適當取捨即可。

我們希望完成類似 JDBC 中的 PrepareStatement 預編譯處理 ,可以使用 #{} ,它會在替換佔位符時首尾新增上單引號 '' ,能有效防止 SQL 注入 風險。

// 1、使用 ${} 有注入風險delete from t_user where id = ${id}// 2、正常傳值,id 傳入 1  delete from t_user where id = 1// 結果刪除了id=1 的記錄    // 3、注入風險,id 傳入 1 or 1=1 delete from t_user where id = 1 or 1=1// 全表刪除了

再看看 #{} 是如何規避 SQL 注入 的:

// 1、使用 #{} 有效防止注入風險delete from t_user where id = #{id}// 2、正常傳值,id 傳入 1   delete from t_user where id = '1'// 結果刪除了id=1 的記錄    // 3、注入風險,id 傳入 1 or 1=1 delete from t_user where id = '1 or 1=1'// SQL 語句報錯,表資料安全

雖然在防止 SQL 注入方面,確實無能為力,不過我們{} 確實無能為力,不過我們 確實無能為力,不過我們{} 在其它方面可不容小覷,例如它允許你靈活地進行 動態表和動態列名的替換 操作,例如:

// 1、靈活查詢指定表資料select * from ${tableName} // 傳入 tableName引數 = t_user , 結果select * from t_user  // 2、靈活查詢不同列條件資料select * from t_user where ${colunmName} = ${value}// 傳入 colunmName引數 = name , value引數 = '潘潘', 結果select * from t_user where name = '潘潘'// 傳入 colunmName引數 = id , value引數 = 1, 結果select * from t_user where id = 1

以上的 {} 替換列名與表名的方式非常靈活,不過確實存在 SQL 注入風險,所以在考慮使用 #{} 或 {} 前,需要評估風險,避免風險,允許的情況下,我建議使用 #{} 。

當然,select 元素允許你配置很多屬性來配置每條語句的行為細節。

<select       id="selectUser"   parameterType="int"  parameterMap="deprecated"  resultType="hashmap"  resultMap="personResultMap"  flushCache="false"  useCache="true"  timeout="10"  fetchSize="256"  statementType="PREPARED"  resultSetType="FORWARD_ONLY"  databaseId="mysql"  resultOrdered="false"  resultSets="rs1,rs2,rs3">  select * from t_user</select>

下面詳細介紹一下,略微冗長,一口氣看完吧:

id 必填項,在名稱空間下的唯一標識,可被 Mybatis 引用,如果存在相同的 “ 名稱空間 + 語句id ” 組合,Mybatis 將丟擲異常;parameterType 可選項,傳入語句的引數的類全限定名或別名,可以是基本型別、map 或 JavaBean 等複雜的引數型別傳遞給 SQL;parameterMap 用於引用外部 parameterMap 的屬性塊,目前已被廢棄。以後請使用行內參數對映和 parameterType 屬性。resultType 可選項,定義類的全路徑,在允許自動匹配的情況下,結果集將透過 Javaben 的規範對映,或定義為 int 、double、float 等引數;也可以使用別名,但是要符合別名規範和定義。 resultType 和 resultMap 之間只能同時使用一個。(日常中,比如我們統計結果總條數的時候可以設定為 int );resultMap 可選項,對外部 resultMap 的命名引用。結果對映是 MyBatis 最強大的特性,如果你對其理解透徹,許多複雜的對映問題都能迎刃而解,後面一對一、一對多、多對多我們會有一篇文章單獨講解。 resultType 和 resultMap 之間只能同時使用一個。flushCache 可選項,清空快取,將其設定為 true 後,只要語句被呼叫,都會導致本地快取和二級快取被清空,預設值:false。useCache 可選項,使用快取,將其設定為 true 後,將會導致本條語句的結果被二級快取快取起來,預設值:對 select 元素為 true。timeout 可選項,這個設定是在丟擲異常之前,驅動程式等待資料庫返回請求結果的秒數。預設值為未設定(unset)(依賴資料庫驅動)。fetchSize 可選項,獲取記錄的總條數設定。這是一個給驅動的建議值,嘗試讓驅動程式每次批次返回的結果行數等於這個設定值。 預設值為未設定(unset)(依賴驅動)。由於效能問題,建議在 sql 做分頁處理。statementType 可選項,可選 STATEMENT,PREPARED 或 CALLABLE。這會讓 MyBatis 分別使用 Statement,PreparedStatement 或 CallableStatement,預設值:PREPARED。resultSetType 可選項,FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等價於 unset) 中的一個,預設值為 unset (依賴資料庫驅動)。 FORWARD_ONLY,只允許遊標向前訪問; SCROLL_SENSITIVE,允許遊標雙向滾動,但不會及時更新資料,也就是說如果資料庫中的資料被修改過,並不會在resultSet中及時更新出來; SCROLL_INSENSITIVE ,允許遊標雙向滾動,如果資料庫中的資料被修改過,會及時更新到resultSet;

我們知道 JDBC 透過 ResultSet 來對查詢結果進行封裝,ResultSet 物件本身包含了一個由查詢語句返回的一個結果集合。例如你經常在 JDBC 見過的結果集讀取:

// 允許滾動遊標索引結果集while( rs.next() ){    rs.getString("name");}// 當然也支援遊標定位到最後一個位置rs.last();// 向後滾動rs.previous();
databaseId 可選項,如果配置了資料庫廠商標識(databaseIdProvider),MyBatis 會載入所有不帶 databaseId 或匹配當前 databaseId 的語句;如果帶和不帶的語句都有,則不帶的會被忽略。resultOrdered 可選項,這個設定僅針對巢狀結果 select 語句:如果為 true,將會假設包含了巢狀結果集或是分組,當返回一個主結果行時,就不會產生對前面結果集的引用。 這就使得在獲取巢狀結果集的時候不至於記憶體不夠用。預設值:false。resultSets 這個設定僅適用於多結果集的情況。它將列出語句執行後返回的結果集並賦予每個結果集一個名稱,多個名稱之間以逗號分隔。四、insert / update / delete 增刪改

資料變更語句 insert,update 和 delete 的實現非常接近,而且相對於 select 元素而言要簡單許多。

<insert  id="insertUser"  parameterType="domain.vo.User"  flushCache="true"  statementType="PREPARED"  keyProperty=""  keyColumn=""  useGeneratedKeys=""  timeout="20"><update  id="updateUser"  parameterType="domain.vo.User"  flushCache="true"  statementType="PREPARED"  timeout="20"><delete  id="deleteUser"  parameterType="domain.vo.User"  flushCache="true"  statementType="PREPARED"  timeout="20">

其中大部分屬性和 select 元素相同,我們介紹 3 個不同的屬性:

useGeneratedKeys : (僅適用於 insert 和 update)這會令 MyBatis 使用 JDBC 的 getGeneratedKeys 方法來取出由資料庫內部生成的主鍵(比如:像 MySQL 和 SQL Server 這樣的關係型資料庫管理系統的自動遞增欄位),預設值:false。keyProperty : (僅適用於 insert 和 update)指定能夠唯一識別物件的屬性,MyBatis 會使用 getGeneratedKeys 的返回值或 insert 語句的 selectKey 子元素設定它的值,預設值:未設定(unset)。如果生成列不止一個,可以用逗號分隔多個屬性名稱。keyColumn : (僅適用於 insert 和 update)設定生成鍵值在表中的列名,在某些資料庫(像 PostgreSQL)中,當主鍵列不是表中的第一列的時候,是必須設定的。如果生成列不止一個,可以用逗號分隔多個屬性名稱。

我們先看看 insert,update 和 delete 語句的示例:

<insert id="insertUser">  insert into t_user (id,name)   values (#{id},#{name})</insert><update id="updateUser">  update t_user set name = #{name} where id = #{id}</update><delete id="deleteUser">  delete from t_user where id = #{id}</delete>

如前所述,插入語句的配置規則更加豐富,在插入語句裡面有一些額外的屬性和子元素用來處理主鍵的生成,並且提供了多種生成方式。

首先,如果你的資料庫支援 自動生成主鍵 的欄位(比如 MySQL 和 SQL Server),那麼你可以設定 useGeneratedKeys=”true”,然後再把 keyProperty 設定為目標屬性就 OK 了。例如,如果上面的 t_user 表已經在 id 列上使用了自動生成,那麼語句可以修改為:

<insert id="insertUser" useGeneratedKeys="true"    keyProperty="id">  insert into t_user (name) values (#{name})</insert>

如果你的資料庫還支援多行插入, 你也可以傳入一個 User 陣列或集合,並返回自動生成的主鍵。

<insert id="insertUser" useGeneratedKeys="true"    keyProperty="id">    insert into t_user (name) values        <foreach item="item" collection="list" separator=",">    (#{item.name})  </foreach></insert>

對於不支援自動生成主鍵列的資料庫和可能不支援自動生成主鍵的 JDBC 驅動,MyBatis 有另外一種方法來生成主鍵

這裡有一個簡單(也很傻)的示例,它可以生成一個隨機 ID(不建議實際使用,這裡只是為了展示 MyBatis 處理問題的靈活性和寬容度):

<insert id="insertUser">   <selectKey keyProperty="id" resultType="int" order="BEFORE">    select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1  </selectKey>      insert into t_user (id, name)  values  (#{id}, #{name})</insert>

在上面的示例中,首先會執行 selectKey 元素中的語句,並設定 User 的 id,然後才會呼叫插入語句。這樣就實現了資料庫自動生成主鍵類似的行為,同時保持了 Java 程式碼的簡潔。

selectKey 元素描述如下:

<selectKey  keyProperty="id"  resultType="int"  order="BEFORE"  statementType="PREPARED">

selectKey 中的 order 屬性有2個選擇:BEFORE 和 AFTER 。

BEFORE:表示先執行selectKey的語句,然後將查詢到的值設定到 JavaBean 對應屬性上,然後再執行 insert 語句。AFTER:表示先執行 AFTER 語句,然後再執行 selectKey 語句,並將 selectKey 得到的值設定到 JavaBean 中的屬性。上面示例中如果改成 AFTER,那麼插入的 id 就會是空值,但是返回的 JavaBean 屬性內會有值。五、cache 快取

快取對於網際網路系統來說特別常見,其特點就是將資料儲存在記憶體中。MyBatis 內建了一個強大的事務性查詢快取機制,它可以非常方便地配置和定製。 為了使它更加強大而且易於配置,我們對 MyBatis 3 中的快取實現進行了許多改進。

預設情況下,只啟用了本地的會話快取(即一級快取,sqlSession級別 ),它僅僅對一個會話中的資料進行快取。 要啟用全域性的二級快取,首先在全域性配置檔案config.xml檔案中加入如下程式碼:

對映語句檔案中的所有 select 語句的結果將會被快取。對映語句檔案中的所有 insert、update 和 delete 語句會重新整理快取。快取會使用最近最少使用演算法(LRU, Least Recently Used)演算法來清除不需要的快取。快取不會定時進行重新整理(也就是說,沒有重新整理間隔)。快取會儲存列表或物件(無論查詢方法返回哪種)的 1024 個引用。快取會被視為讀/寫快取,這意味著獲取到的物件並不是共享的,可以安全地被呼叫者修改,而不干擾其他呼叫者或執行緒所做的潛在修改。

快取只作用於 cache 標籤所在的對映檔案中的語句。如果你混合使用 Java API 和 XML 對映檔案,在共用介面中的語句將不會被預設快取。你需要使用 @CacheNamespaceRef 註解指定快取作用域。

這些屬性可以透過 cache 元素的屬性來修改。比如:

<cache  eviction="FIFO"  flushInterval="60000"  size="512"  readOnly="true"/>

上面表示了一套更高階的快取配置,首先建立了一個 FIFO 快取,每隔 60 秒重新整理,最多可以儲存結果物件或列表的 512 個引用,然後返回的物件被設定成只讀的,因此對它們進行修改可能會在不同執行緒中的呼叫者產生衝突。

快取可用的清除策略有:

LRU – 最近最少使用:移除最長時間不被使用的物件。FIFO – 先進先出:按物件進入快取的順序來移除它們。SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除物件。WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除物件。

預設的清除策略是 LRU

flushInterval(重新整理間隔)屬性可以被設定為任意的正整數,設定的值應該是一個以毫秒為單位的合理時間量。 預設情況是不設定,也就是沒有重新整理間隔,快取僅僅會在呼叫語句時重新整理。

size(引用數目)屬性可以被設定為任意正整數,要注意欲快取物件的大小和執行環境中可用的記憶體資源。預設值是 1024。

readOnly(只讀)屬性可以被設定為 true 或 false。只讀的快取會給所有呼叫者返回快取物件的相同例項。 因此這些物件不能被修改。這就提供了可觀的效能提升。而可讀寫的快取會(透過序列化)返回快取物件的複製。 速度上會慢一些,但是更安全,因此預設值是 false。

二級快取是事務性的。這意味著,當 SqlSession 完成並提交 ( commit ) 時,或是完成並回滾 ( close ) 時,二級快取都會被重新整理。不管是否配置了 flushCache=true 。

Mybatis 的快取包括一級快取(sqlSession 級別)和二級快取(mapper 級別),所以 mapper 對映器中配置的是二級快取,我們先大概知道有這個概念,因為後續我們會針對這兩種快取進行詳細介紹,而且還會講解如何自定義快取,因為 Mybatis 的快取預設都是以 map 的資料結構儲存在本地,所以自定義快取可以把儲存介質拓展到磁碟或資料庫redis等;而且一級快取是預設開啟的,二級快取需要我們手工開啟,這些後續都會詳細講解,提前預告。

快取獲取順序:二級快取 > 一級快取 > 資料庫

六、cache-ref 引用快取

回想一下 cache 的內容,對某一名稱空間的語句,只會使用該名稱空間的快取進行快取或重新整理。 但你可能會想要在多個名稱空間中共享相同的快取配置和例項。要實現這種需求,你可以使用 cache-ref 元素來引用另一個快取。

<cache-ref namespace="com.vo.UserMapper"/>
七、sql 語句塊

這個元素可以用來定義可重用的 SQL 程式碼片段,以便在其它語句中使用。 引數可以靜態地(在載入的時候)確定下來,並且可以在不同的 include 元素中定義不同的引數值。比如:

<sql id="userColumns">     ${alias}.id,${alias}.name </sql>

這個 SQL 片段可以在其它語句中使用,例如:

<select id="selectUsers" resultType="map">  select    <include refid="userColumns">        <property name="alias" value="t1"/>    </include>,    <include refid="userColumns">        <property name="alias" value="t2"/>    </include>  from t_user t1 cross join t_user t2</select>

也可以在 include 元素的 refid 屬性或多層內部語句中使用屬性值,例如:

<sql id="sql1">  ${prefix}_user</sql><sql id="sql2">  from    <include refid="${include_target}"/></sql><select id="select" resultType="map">  select    id, name  <include refid="sql2">    <property name="prefix" value="t"/>    <property name="include_target" value="sql1"/>  </include></select>
八、parameterMap 引數對映

parameterMap 元素官方已經不建議使用,並且再後續版本會退出舞臺。首先對於我們 Java 來說,特別不希望在程式碼中透過傳遞 map 來傳參,這樣對於後續維護或者引數查詢都是極不負責任的,我們推薦使用 JavaBean 來傳值引數,這是 parameterMap 被拋棄的其中一個原因;另外也由於 parameterType 屬性的誕生就能很好的代替 parameterMap ,並且還能自定義 JavaBean 型別的傳參,所以 parameterMap 退出舞臺,實屬正常。

九、總結

我一直來都希望自己只輸出觀點,而不是輸出字典,但其中有些知識點又是極其冗雜,知識輸出真是個難搞的差事,如何既能把知識脈絡梳理的完整,又能講得淺顯易懂,言簡意賅,確實是後續文章分解輸出的研究方向。

連結:https://juejin.cn/post/6907274537113485325

22
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • kubernetes+Jenkins持續整合