主頁 > 後端開發 > Mybatis系列全解(八):Mybatis的9大動態SQL標簽你知道幾個?提前致女神!

Mybatis系列全解(八):Mybatis的9大動態SQL標簽你知道幾個?提前致女神!

2021-03-05 06:23:06 後端開發

封面:洛小汐

作者:潘潘

2021年,仰望天空,腳踏實地,

這算是春節后首篇 Mybatis 文了~

跨了個年感覺寫了有半個世紀 ...

借著女神節 ヾ(?°?°?)??

提前祝男神女神們越靚越富越嗨森!

上圖保存可做朋友圈封面圖 ~

前言

本節我們介紹 Mybatis 的強大特性之一:動態 SQL ,從動態 SQL 的誕生背景與基礎概念,到動態 SQL 的標簽成員及基本用法,我們徐徐道來,再結合框架原始碼,剖析動態 SQL (標簽)的底層原理,最終在文末吐槽一下:在無動態 SQL 特性(標簽)之前,我們會常常掉進哪些可惡的坑吧~

建議關注我們! Mybatis 全解系列一直在更新哦

Mybaits系列全解

  • Mybatis系列全解(一):手寫一套持久層框架
  • Mybatis系列全解(二):Mybatis簡介與環境搭建
  • Mybatis系列全解(三):Mybatis簡單CRUD使用介紹
  • Mybatis系列全解(四):全網最全!Mybatis組態檔XML全貌詳解
  • Mybatis系列全解(五):全網最全!詳解Mybatis的Mapper映射檔案
  • Mybatis系列全解(六):Mybatis最硬核的API你知道幾個?
  • Mybatis系列全解(七):Dao層的兩種實作之傳統與代理
  • Mybatis系列全解(八):Mybatis的動態SQL
  • Mybatis系列全解(九):Mybatis的復雜映射
  • Mybatis系列全解(十):Mybatis注解開發
  • Mybatis系列全解(十一):Mybatis快取全解
  • Mybatis系列全解(十二):Mybatis插件開發
  • Mybatis系列全解(十三):Mybatis代碼生成器
  • Mybatis系列全解(十四):Spring集成Mybatis
  • Mybatis系列全解(十五):SpringBoot集成Mybatis
  • Mybatis系列全解(十六):Mybatis原始碼剖析

本文目錄

1、什么是動態SQL

2、動態SQL的誕生記

3、動態SQL標簽的9大標簽

4、動態SQL的底層原理

1、什么是動態SQL ?

關于動態 SQL ,允許我們理解為 “ 動態的 SQL ”,其中 “ 動態的 ” 是形容詞,“ SQL ” 是名詞,那顯然我們需要先理解名詞,畢竟形容詞僅僅代表它的某種形態或者某種狀態,

SQL 的全稱是:

Structured Query Language,結構化查詢語言,

SQL 本身好說,我們小學時候都學習過了,無非就是 CRUD 嘛,而且我們還知道它是一種 語言,語言是一種存在于物件之間用于交流表達的 能力,例如跟中國人交流用漢語、跟英國人交流用英語、跟火星人交流用火星語、跟小貓交流用喵喵語、跟計算機交流我們用機器語言、跟資料庫管理系統(DBMS)交流我們用 SQL,

想必大家立馬就能明白,想要與某個物件交流,必須擁有與此物件交流的語言能力才行!所以無論是技術人員、還是應用程式系統、或是某個高級語言環境,想要訪問/操作資料庫,都必須具備 SQL 這項能力;因此你能看到像 Java ,像 Python ,像 Go 等等這些高級語言環境中,都會嵌入(支持) SQL 能力,達到與資料庫互動的目的,

很顯然,能夠學習 Mybatis 這么一門高精尖(ru-men)持久層框架的編程人群,對于 SQL 的撰寫能力肯定已經掌握得 ss 的,平時各種 SQL 撰寫那都是信手拈來的事, 只不過對于 動態SQL 到底是個什么東西,似憾訓有一些朋友似懂非懂!但是沒關系,我們百度一下,

動態 SQL:一般指根據用戶輸入或外部條件 動態組合 的 SQL 陳述句塊,

很容易理解,隨外部條件動態組合的 SQL 陳述句塊!我們先針對動態 SQL 這個詞來剖析,世間萬物,有動態那就相對應的有靜態,那么他們的邊界在哪里呢?又該怎么區分呢?

其實,上面我們已經介紹過,在例如 Java 高級語言中,都會嵌入(支持)SQL 能力,一般我們可以直接在代碼或組態檔中撰寫 SQL 陳述句,如果一個 SQL 陳述句在 “編譯階段” 就已經能確定 主體結構,那我們稱之為靜態 SQL,如果一個 SQL 陳述句在編譯階段無法確定主體結構,需要等到程式真正 “運行時” 才能最終確定,那么我們稱之為動態 SQL,舉個例子:

<!-- 1、定義SQL -->
<mapper namespace="dao">
	<select id="selectAll" resultType="user">
    	select * from t_user
	</select>
</mapper>
// 2、執行SQL
sqlSession.select("dao.selectAll");

很明顯,以上這個 SQL ,在編譯階段我們都已經知道它的主體結構,即查詢 t_user 表的所有記錄,而無需等到程式運行時才確定這個主體結構,因此以上屬于 靜態 SQL,那我們再看看下面這個陳述句:

<!-- 1、定義SQL -->
<mapper namespace="dao">
	<select id="selectAll" parameterType="user">
    	select * from t_user 
        <if test="id != null">
            where id = #{id}
        </if>
	</select>
</mapper>
// 2、執行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

User user2 = new User(); 
sqlSession.select("dao.selectAll",user2);  // 無 id

認真觀察,以上這個 SQL 陳述句,額外添加了一塊 if 標簽 作為條件判斷,所以應用程式在編譯階段是無法確定 SQL 陳述句最終主體結構的,只有在運行時根據應用程式是否傳入 id 這個條件,來動態的拼接最終執行的 SQL 陳述句,因此屬于動態 SQL ,

另外,還有一種常見的情況,大家看看下面這個 SQL 陳述句算是動態 SQL 陳述句嗎?

<!-- 1、定義SQL -->
<mapper namespace="dao">
	<select id="selectAll" parameterType="user">
    	select * from t_user where id = #{id} 
	</select>
</mapper>
// 2、執行SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

根據動態 SQL 的定義,大家是否能判斷以上的陳述句塊是否屬于動態 SQL?

答案:不屬于動態 SQL !

原因很簡單,這個 SQL 在編譯階段就已經明確主體結構了,雖然外部動態的傳入一個 id ,可能是1,可能是2,可能是100,但是因為它的主體結構已經確定,這個陳述句就是查詢一個指定 id 的用戶記錄,它最終執行的 SQL 陳述句不會有任何動態的變化,所以頂多算是一個支持動態傳參的靜態 SQL ,

至此,我們對于動態 SQL 和靜態 SQL 的區別已經有了一個基礎認知,但是有些好奇的朋友又會思考另一個問題:動態 SQL 是 Mybatis 獨有的嗎?

2、動態SQL的誕生記

我們都知道,SQL 是一種偉大的資料庫語言 標準,在資料庫管理系統紛爭的時代,它的出現統一規范了資料庫操作語言,而此時,市面上的資料庫管理軟體百花齊放,我最早使用的 SQL Server 資料庫,當時用的資料庫管理工具是 SQL Server Management Studio,后來接觸 Oracle 資料庫,用了 PL/SQL Developer,再后來直至今日就幾乎都在用 MySQL 資料庫(這個跟各種云廠商崛起有關),所以基本使用 Navicat 作為資料庫管理工具,當然如今市面上還有許多許多,資料庫管理工具嘛,只要能便捷高效的管理我們的資料庫,那就是好工具,duck 不必糾結選擇哪一款!

那這么多好工具,都提供什么功能呢?相信我們平時接觸最多的就是接收執行 SQL 陳述句的輸入界面(也稱為查詢編輯器),這個輸入界面幾乎支持所有 SQL 語法,例如我們撰寫一條陳述句查詢 id 等于15 的用戶資料記錄:

select * from user where id = 15 ;

我們來看一下這個查詢結果:

很顯然,在這個輸入界面內輸入的任何 SQL 陳述句,對于資料庫管理工具來說,都是 動態 SQL!因為工具本身并不可能提前知道用戶會輸入什么 SQL 陳述句,只有當用戶執行之后,工具才接收到用戶實際輸入的 SQL 陳述句,才能最終確定 SQL 陳述句的主體結構,當然!即使我們不通過可視化的資料庫管理工具,也可以用資料庫本身自帶支持的命令列工具來執行 SQL 陳述句,但無論用戶使用哪類工具,輸入的陳述句都會被工具認為是 動態 SQL

這么一說,動態 SQL 原來不是 Mybatis 獨有的特性!其實除了以上介紹的資料庫管理工具以外,在純 JDBC 時代,我們就經常通過字串來動態的拼接 SQL 陳述句,這也是在高級語言環境(例如 Java 語言編程環境)中早期常用的動態 SQL 構建方式!

// 外部條件id
Integer id = Integer.valueOf(15);

// 動態拼接SQL
StringBuilder sql = new StringBuilder();
sql.append(" select  *   ");
sql.append("   from user ");

// 根據外部條件id動態拼接SQL
if ( null != id ){
    sql.append(" where id = " + id);
}

// 執行陳述句
connection.prepareStatement(sql);

只不過,這種構建動態 SQL 的方式,存在很大的安全問題和例外風險(我們第5點會詳細介紹),所以不建議使用,后來 Mybatis 入世之后,在對待動態 SQL 這件事上,就格外上心,它默默發誓,一定要為使用 Mybatis 框架的用戶提供一套棒棒的方案(標簽)來靈活構建動態 SQL!

于是乎,Mybatis 借助 OGNL 的運算式的偉大設計,可算在動態 SQL 構建方面提供了各類功能強大的輔助標簽,我們簡單列舉一下有:if、choose、when、otherwise、trim、where、set、foreach、bind等,我隨手翻了翻我電腦里頭曾經保存的學習筆記,我們一起在第3節中溫故知新,詳細的講一講吧~

另外,需要糾正一點,就是我們平日里在 Mybatis 框架中常說的動態 SQL ,其實特指的也就是 Mybatis 框架中的這一套動態 SQL 標簽,或者說是這一 特性,而并不是在說動態 SQL 本身,

3、動態SQL標簽的9大標簽

很好,可算進入我們動態 SQL 標簽的主題,根據前面的鋪墊,其實我們都能發現,很多時候靜態 SQL 陳述句并不能滿足我們復雜的業務場景需求,所以我們需要有適當靈活的一套方式或者能力,來便捷高效的構建動態 SQL 陳述句,去匹配我們動態變化的業務需求,舉個栗子,在下面此類多條件的場景需求之下,動態 SQL 陳述句就顯得尤為重要(先登場 if 標簽),

當然,很多朋友會說這類需求,不能用 SQL 來查,得用搜索引擎,確實如此,但是呢,在我們的實際業務需求當中,還是存在很多沒有引入搜索引擎系統,或者有些根本無需引入搜索引擎的應用程式或功能,它們也會涉及到多選項多條件或者多結果的業務需求,那此時也就確實需要使用動態 SQL 標簽來靈活構建執行陳述句,

那么, Mybatis 目前都提供了哪些棒棒的動態 SQL 標簽呢 ?我們先引出一個類叫做 XMLScriptBuilder ,大家先簡單理解它是負責決議我們的動態 SQL 標簽的這么一個構建器,在第4點底層原理中我們再詳細介紹,

// XML腳本標簽構建器
public class XMLScriptBuilder{
    
    // 標簽節點處理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
    
    // 構造器
    public XMLScriptBuilder() { 
        initNodeHandlerMap();
        //... 其它初始化不贅述也不重要
    }
    
    // 初始化
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
}

其實原始碼中很清晰得體現,一共有 9 大動態 SQL 標簽!Mybatis 在初始化決議組態檔的時候,會實體化這么一個標簽節點的構造器,那么它本身就會提前把所有 Mybatis 支持的動態 SQL 標簽物件對應的處理器給進行一個實體化,然后放到一個 Map 池子里頭,而這些處理器,都是該類 XMLScriptBuilder 的一個匿名內部類,而匿名內部類的功能也很簡單,就是決議處理對應型別的標簽節點,在后續應用程式使用動態標簽的時候,Mybatis 隨時到 Map 池子中匹配對應的標簽節點處理器,然后進決議即可,下面我們分別對這 9 大動態 SQL 標簽進行介紹,排(gen)名(ju)不(wo)分(de)先(xi)后(hao):


Top1、if 標簽

常用度:★★★★★

實用性:★★★★☆

if 標簽,絕對算得上是一個偉大的標簽,任何不支持流程控制(或陳述句控制)的應用程式,都是耍流氓,幾乎都不具備現實意義,實際的應用場景和流程必然存在條件的控制與流轉,而 if 標簽在 單條件分支判斷 應用場景中就起到了舍我其誰的作用,語法很簡單,如果滿足,則執行,不滿足,則忽略/跳過,

  • if 標簽 : 內嵌于 select / delete / update / insert 標簽,如果滿足 test 屬性的條件,則執行代碼塊
  • test 屬性 :作為 if 標簽的屬性,用于條件判斷,使用 OGNL 運算式,

舉個例子:

<select id="findUser">
    select * from User where 1=1
    <if test=" age != null ">
        and age > #{age}
    </if>
    <if test=" name != null ">
        and name like concat(#{name},'%')
    </if>
</select>

很明顯,if 標簽元素常用于包含 where 子句的條件拼接,它相當于 Java 中的 if 陳述句,和 test 屬性搭配使用,通過判斷引數值來決定是否使用某個查詢條件,也可用于 Update 陳述句中判斷是否更新某個欄位,或用于 Insert 陳述句中判斷是否插入某個欄位的值,

每一個 if 標簽在進行單條件判斷時,需要把判斷條件設定在 test 屬性中,這是一個常見的應用場景,我們常用的用戶查詢系統功能中,在前端一般提供很多可選的查詢項,支持性別篩選、年齡區間篩查、姓名模糊匹配等,那么我們程式中接收用戶輸入之后,Mybatis 的動態 SQL 節省我們很多作業,允許我們在代碼層面不進行引數邏輯處理和 SQL 拼接,而是把引數傳入到 SQL 中進行條件判斷動態處理,我們只需要把精力集中在 XML 的維護上,既靈活也方便維護,可讀性還強,

有些心細的朋友可能就發現一個問題,為什么 where 陳述句會添加一個 1=1 呢?其實我們是為了方便拼接后面符合條件的 if 標簽陳述句塊,否則沒有 1=1 的話我們拼接的 SQL 就會變成 select * from user where and age > 0 , 顯然這不是我們期望的結果,當然也不符合 SQL 的語法,資料庫也不可能執行成功,所以我們投機取巧添加了 1=1 這個陳述句,但是始終覺得多余且沒必要,Mybatis 也考慮到了,所以等會我們講 where 標簽,它是如何完美解決這個問題的,

注意:if 標簽作為單條件分支判斷,只能控制與非此即彼的流程,例如以上的例子,如果年齡 age 和姓名 name 都不存在,那么系統會把所有結果都查詢出來,但有些時候,我們希望系統更加靈活,能有更多的流程分支,例如像我們 Java 當中的 if else 或 switch case default,不僅僅只有一個條件分支,所以接下來我們介紹 choose 標簽,它就能滿足多分支判斷的應用場景,


Top2、choose 標簽、when 標簽、otherwise 標簽

常用度:★★★★☆

實用性:★★★★☆

有些時候,我們并不希望條件控制是非此即彼的,而是希望能提供多個條件并從中選擇一個,所以貼心的 Mybatis 提供了 choose 標簽元素,類似我們 Java 當中的 if else 或 switch case default,choose 標簽必須搭配 when 標簽和 otherwise 標簽使用,驗證條件依然是使用 test 屬性進行驗證,

  • choose 標簽:頂層的多分支標簽,單獨使用無意義
  • when 標簽:內嵌于 choose 標簽之中,當滿足某個 when 條件時,執行對應的代碼塊,并終止跳出 choose 標簽,choose 中必須至少存在一個 when 標簽,否則無意義
  • otherwise 標簽:內嵌于 choose 標簽之中,當不滿足所有 when 條件時,則執行 otherwise 代碼塊,choose 中 至多 存在一個 otherwise 標簽,可以不存在該標簽
  • test 屬性 :作為 when 與 otherwise 標簽的屬性,作為條件判斷,使用 OGNL 運算式

依據下面的例子,當應用程式輸入年齡 age 或者姓名 name 時,會執行對應的 when 標簽內的代碼塊,如果 when 標簽的年齡 age 和姓名 name 都不滿足,則會拼接 otherwise 標簽內的代碼塊,

<select id="findUser">
    select * from User where 1=1 
    <choose>
        <when test=" age != null ">
        	and age > #{age}
        </when>
        <when test=" name != null ">
        	and name like concat(#{name},'%')
        </when>
        <otherwise>
            and sex = '男'
        </otherwise>
    </choose>
</select>

很明顯,choose 標簽作為多分支條件判斷,提供了更多靈活的流程控制,同時 otherwise 的出現也為程式流程控制兜底,有時能夠避免部分系統風險、過濾部分條件、避免當程式沒有匹配到條件時,把整個資料庫資源全部查詢或更新,

至于為何 choose 標簽這么棒棒,而常用度還是比 if 標簽少了一顆星呢?原因也簡單,因為 choose 標簽的很多使用場景可以直接用 if 標簽代替,另外據我統計,if 標簽在實際業務應用當中,也要多于 choose 標簽,大家也可以具體核查自己的應用程式中動態 SQL 標簽的占比情況,統計分析一下,


Top3、foreach 標簽

常用度:★★★☆☆

實用性:★★★★☆

有些場景,可能需要查詢 id 在 1 ~ 100 的用戶記錄

有些場景,可能需要批量插入 100 條用戶記錄

有些場景,可能需要更新 500 個用戶的姓名

有些場景,可能需要你洗掉 10 條用戶記錄

請問大家

很多增刪改查場景,操作物件都是集合/串列

如果是你來設計支持 Mybatis 的這一類集合/串列遍歷場景,你會提供什么能力的標簽來輔助構建你的 SQL 陳述句從而去滿足此類業務場景呢?

額(⊙o⊙)…

那如果一定要用 Mybatis 框架呢?

沒錯,確實 Mybatis 提供了 foreach 標簽來處理這幾類需要遍歷集合的場景,foreach 標簽作為一個回圈陳述句,他能夠很好的支持陣列、Map、或實作了 Iterable 介面(List、Set)等,尤其是在構建 in 條件陳述句的時候,我們常規的用法都是 id in (1,2,3,4,5 ... 100) ,理論上我們可以在程式代碼中拼接字串然后通過 ${ ids } 方式來傳值獲取,但是這種方式不能防止 SQL 注入風險,同時也特別容易拼接錯誤,所以我們此時就需要使用 #{} + foreach 標簽來配合使用,以滿足我們實際的業務需求,譬如我們傳入一個 List 串列查詢 id 在 1 ~ 100 的用戶記錄:

<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

最終拼接完整的陳述句就變成:


select  * from user where ids in (1,2,3,...,100);

當然你也可以這樣撰寫:

<select id="findAll">
    select  * from user where 
    <foreach collection="list"
        item="item" index="index" 
        open=" " separator=" or " close=" ">
            id = #{item}
    </foreach>
</select>

最終拼接完整的陳述句就變成:


select  * from user where id =1 or id =2 or id =3  ... or id = 100;

在資料量大的情況下這個性能會比較尷尬,這里僅僅做一個用法的舉例,所以經過上面的舉栗,相信大家也基本能猜出 foreach 標簽元素的基本用法:

  • foreach 標簽:頂層的遍歷標簽,單獨使用無意義
  • collection 屬性:必填,Map 或者陣列或者串列的屬性名(不同型別的值獲取下面會講解)
  • item 屬性:變數名,值為遍歷的每一個值(可以是物件或基礎型別),如果是物件那么依舊是 OGNL 運算式取值即可,例如 #{item.id} 、#{ user.name } 等
  • index 屬性:索引的屬性名,在遍歷串列或陣列時為當前索引值,當迭代的物件時 Map 型別時,該值為 Map 的鍵值(key)
  • open 屬性:回圈內容開頭拼接的字串,可以是空字串
  • close 屬性:回圈內容結尾拼接的字串,可以是空字串
  • separator 屬性:每次回圈的分隔符

第一,當傳入的引數為 List 物件時,系統會默認添加一個 key 為 'list' 的值,把串列內容放到這個 key 為 list 的集合當中,在 foreach 標簽中可以直接通過 collection="list" 獲取到 List 物件,無論你傳入時使用 kkk 或者 aaa ,都無所謂,系統都會默認添加一個 key 為 list 的值,并且 item 指定遍歷的物件值,index 指定遍歷索引值,

// java 代碼
List kkk = new ArrayList();
kkk.add(1);
kkk.add(2);
...
kkk.add(100);
sqlSession.selectList("findAll",kkk);
<!-- xml 配置 -->
<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

第二,當傳入的引數為陣列時,系統會默認添加一個 key 為 'array' 的值,把串列內容放到這個 key 為 array 的集合當中,在 foreach 標簽中可以直接通過 collection="array" 獲取到陣列物件,無論你傳入時使用 ids 或者 aaa ,都無所謂,系統都會默認添加一個 key 為 array 的值,并且 item 指定遍歷的物件值,index 指定遍歷索引值,

// java 代碼
String [] ids = new String[3];
ids[0] = "1";
ids[1] = "2";
ids[2] = "3";
sqlSession.selectList("findAll",ids);
<!-- xml 配置 -->
<select id="findAll">
    select  * from user where ids in 
    <foreach collection="array"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

第三,當傳入的引數為 Map 物件時,系統并 不會 默認添加一個 key 值,需要手工傳入,例如傳入 key 值為 map2 的集合物件,在 foreach 標簽中可以直接通過 collection="map2" 獲取到 Map 物件,并且 item 代表每次迭代的的 value 值,index 代表每次迭代的 key 值,其中 item 和 index 的值名詞可以隨意定義,例如 item = "value111",index ="key111",

// java 代碼
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3);

Map map1 = new HashMap<>();
map1.put("map2",map2);
sqlSession.selectList("findAll",map1);

挺鬧心,map1 套著 map2,才能在 foreach 的 collection 屬性中獲取到,

<!-- xml 配置 -->
<select id="findAll">
    select  * from user where
    <foreach collection="map2"
        item="value111" index="key111" 
        open=" " separator=" or " close=" ">
        id = #{value111}
    </foreach>
</select>

可能你會覺得 Map 受到不公平對待,為何 map 不能像 List 或者 Array 一樣,在框架默認設定一個 'map' 的 key 值呢?但其實不是不公平,而是我們在 Mybatis 框架中,所有傳入的任何引數都會供背景關系使用,于是引數會被統一放到一個內置引數池子里面,這個內置引數池子的資料結構是一個 map 集合,而這個 map 集合可以通過使用 “_parameter” 來獲取,所有 key 都會存盤在 _parameter 集合中,因此:

  • 當你傳入的引數是一個 list 型別時,那么這個引數池子需要有一個 key 值,以供背景關系獲取這個 list 型別的物件,所以默認設定了一個 'list' 字串作為 key 值,獲取時通過使用 _parameter.list 來獲取,一般使用 list 即可,
  • 同樣的,當你傳入的引數是一個 array 陣列時,那么這個引數池子也會默認設定了一個 'array' 字串作為 key 值,以供背景關系獲取這個 array 陣列的物件值,獲取時通過使用 _parameter.array 來獲取,一般使用 array 即可,
  • 但是!當你傳入的引數是一個 map 集合型別時,那么這個引數池就沒必要為你添加默認 key 值了,因為 map 集合型別本身就會有很多 key 值,例如你想獲取 map 引數的某個 key 值,你可以直接使用 _parameter.name 或者 _parameter.age 即可,就沒必要還用 _parameter.map.name 或者 _parameter.map.age ,所以這就是 map 引數型別無需再構建一個 'map' 字串作為 key 的原因,物件型別也是如此,例如你傳入一個 User 物件,

因此,如果是 Map 集合,你可以這么使用:

// java 代碼
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3); 
sqlSession.selectList("findAll",map2);

直接使用 collection="_parameter",你會發現神奇的 key 和 value 都能通過 _parameter 遍歷在 index 與 item 之中,

<!-- xml 配置 -->
<select id="findAll">
    select  * from user where
    <foreach collection="_parameter"
         item="value111" index="key111"
         open=" " separator=" or " close=" ">
        id = #{value111}
    </foreach>
</select>

延伸:當傳入引數為多個物件時,例如傳入 User 和 Room 等,那么通過內置引數獲取物件可以使用 _parameter.get(0).username,或者 _parameter.get(1).roomname ,假如你傳入的引數是一個簡單資料型別,例如傳入 int =1 或者 String = '你好',那么都可以直接使用 _parameter 代替獲取值即可,這就是很多人會在動態 SQL 中直接使用 # { _parameter } 來獲取簡單資料型別的值,

那到這里,我們基本把 foreach 基本用法介紹完成,不過以上只是針對查詢的使用場景,對于洗掉、更新、插入的用法,也是大同小異,我們簡單說一下,如果你希望批量插入 100 條用戶記錄:

<insert id="insertUser" parameterType="java.util.List">
    insert into user(id,username) values
    <foreach collection="list" 
         item="user" index="index"
         separator="," close=";" >
        (#{user.id},#{user.username})
    </foreach>
</insert>

如果你希望更新 500 個用戶的姓名:

<update id="updateUser" parameterType="java.util.List">
    update user 
       set username = '潘潘' 
     where id in 
    <foreach collection="list"
        item="user" index="index" 
        separator="," open="(" close=")" >
        #{user.id}    
    </foreach>
</update>

如果你希望你洗掉 10 條用戶記錄:

<delete id="deleteUser" parameterType="java.util.List">
    delete from user  
          where id in 
    <foreach collection="list"
         item="user" index="index" 
         separator="," open="(" close=")" >
        #{user.id}    
    </foreach>
</delete>

更多玩法,期待你自己去挖掘!

注意:使用 foreach 標簽時,需要對傳入的 collection 引數(List/Map/Set等)進行為空性判斷,否則動態 SQL 會出現語法例外,例如你的查詢陳述句可能是 select * from user where ids in () ,導致以上語法例外就是傳入引數為空,解決方案可以用 if 標簽或 choose 標簽進行為空性判斷處理,或者直接在 Java 代碼中進行邏輯處理即可,例如判斷為空則不執行 SQL ,


Top4、where 標簽、set 標簽

常用度:★★☆☆☆

實用性:★★★★☆

我們把 where 標簽和 set 標簽放置一起講解,一是這兩個標簽在實際應用開發中常用度確實不分伯仲,二是這兩個標簽出自一家,都繼承了 trim 標簽,放置一起方便我們比對追根,(其中底層原理會在第4部分詳細講解)

之前我們介紹 if 標簽的時候,相信大家都已經看到,我們在 where 子句后面拼接了 1=1 的條件陳述句塊,目的是為了保證后續條件能夠正確拼接,以前在程式代碼中使用字串拼接 SQL 條件陳述句常常如此使用,但是確實此種方式不夠體面,也顯得我們不高級,

<select id="findUser">
    select * from User where 1=1
    <if test=" age != null ">
        and age > #{age}
    </if>
    <if test=" name != null ">
        and name like concat(#{name},'%')
    </if>
</select>

以上是我們使用 1=1 的寫法,那 where 標簽誕生之后,是怎么巧妙處理后續的條件陳述句的呢?

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
            and age > #{age}
        </if>
        <if test=" name != null ">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

我們只需把 where 關鍵詞以及 1=1 改為 < where > 標簽即可,另外還有一個特殊的處理能力,就是 where 標簽能夠智能的去除(忽略)首個滿足條件陳述句的前綴,例如以上條件如果 age 和 name 都滿足,那么 age 前綴 and 會被智能去除掉,無論你是使用 and 運算子或是 or 運算子,Mybatis 框架都會幫你智能處理,

用法特別簡單,我們用官術總結一下

  • where 標簽:頂層的遍歷標簽,需要配合 if 標簽使用,單獨使用無意義,并且只會在子元素(如 if 標簽)回傳任何內容的情況下才插入 WHERE 子句,另外,若子句的開頭為 “AND” 或 “OR”,where 標簽也會將它替換去除,

了解了基本用法之后,我們再看看剛剛我們的例子中:

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
            and age > #{age}
        </if>
        <if test=" name != null ">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

如果 age 傳入有效值 10 ,滿足 age != null 的條件之后,那么就會回傳 where 標簽并去除首個子句運算子 and,最終的 SQL 陳述句會變成:

select * from User where age > 10; 
-- and 巧妙的不見了

值得注意的是,where 標簽 只會 智能的去除(忽略)首個滿足條件陳述句的前綴,所以就建議我們在使用 where 標簽的時候,每個陳述句都最好寫上 and 前綴或者 or 前綴,否則像以下寫法就很有可能出大事:

<select id="findUser">
    select * from User 
    <where>
        <if test=" age != null ">
             age > #{age} 
             <!-- age 前綴沒有運算子-->
        </if>
        <if test=" name != null ">
             name like concat(#{name},'%')
             <!-- name 前綴也沒有運算子-->
        </if>
    </where>
</select>

當 age 傳入 10,name 傳入 ‘潘潘’ 時,最終的 SQL 陳述句是:

select * from User 
where 
age > 10 
name like concat('潘%')
-- 所有條件都沒有and或or運算子
-- 這讓age和name顯得很尷尬~

由于 name 前綴沒有寫 and 或 or 連接符,而 where 標簽又不會智能的去除(忽略)非首個 滿足條件陳述句的前綴,所以當 age 條件陳述句與 name 條件陳述句同時成立時,就會導致語法錯誤,這個需要謹慎使用,格外注意!原則上每個條件子句都建議在句首添加運算子 and 或 or ,首個條件陳述句可添加可不加,

另外還有一個值得注意的點,我們使用 XML 方式配置 SQL 時,如果在 where 標簽之后添加了注釋,那么當有子元素滿足條件時,除了 < !-- --> 注釋會被 where 忽略決議以外,其它注釋例如 // 或 /**/ 或 -- 等都會被 where 當成首個子句元素處理,導致后續真正的首個 AND 子句元素或 OR 子句元素沒能被成功替換掉前綴,從而引起語法錯誤!

基于 where 標簽元素的講解,有助于我們快速理解 set 標簽元素,畢竟它倆是如此相像,我們回憶一下以往我們的更新 SQL 陳述句:

<update id="updateUser">
    update user 
       set age = #{age},
           username = #{username},
           password = #{password} 
     where id =#{id}
</update> 

以上陳述句是我們日常用于更新指定 id 物件的 age 欄位、 username 欄位以及 password 欄位,但是很多時候,我們可能只希望更新物件的某些欄位,而不是每次都更新物件的所有欄位,這就使得我們在陳述句結構的構建上顯得慘白無力,于是有了 set 標簽元素,

用法與 where 標簽元素相似

  • set 標簽:頂層的遍歷標簽,需要配合 if 標簽使用,單獨使用無意義,并且只會在子元素(如 if 標簽)回傳任何內容的情況下才插入 set 子句,另外,若子句的 開頭或結尾 都存在逗號 “,” 則 set 標簽都會將它替換去除,

根據此用法我們可以把以上的例子改為:

<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               age = #{age},
           </if>
           <if test="username !=null">
           	   username = #{username},
           </if> 
           <if test="password !=null">
           	   password = #{password},
           </if>
        </set>    
     where id =#{id}
</update> 

很簡單易懂,set 標簽會智能拼接更新欄位,以上例子如果傳入 age =10 和 username = '潘潘' ,則有兩個欄位滿足更新條件,于是 set 標簽會智能拼接 " age = 10 ," 和 "username = '潘潘' ," ,其中由于后一個 username 屬于最后一個子句,所以末尾逗號會被智能去除,最終的 SQL 陳述句是:

update user set age = 10,username =  '潘潘' 

另外需要注意,set 標簽下需要保證至少有一個條件滿足,否則依然會產生語法錯誤,例如在無子句條件滿足的場景下,最終的 SQL 陳述句會是這樣:

update user ;  ( oh~ no!)

既不會添加 set 標簽,也沒有子句更新欄位,于是語法出現了錯誤,所以類似這類情況,一般需要在應用程式中進行邏輯處理,判斷是否存在至少一個引數,否則不執行更新 SQL ,所以原則上要求 set 標簽下至少存在一個條件滿足,同時每個條件子句都建議在句末添加逗號 ,最后一個條件陳述句可加可不加,或者 每個條件子句都在句首添加逗號 ,第一個條件陳述句可加可不加,例如:

<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               ,age = #{age}
           </if>
           <if test="username !=null">
           	   ,username = #{username}
           </if> 
           <if test="password !=null">
           	   ,password = #{password}
           </if>
        </set>    
     where id =#{id}
</update> 

與 where 標簽相同,我們使用 XML 方式配置 SQL 時,如果在 set 標簽子句末尾添加了注釋,那么當有子元素滿足條件時,除了 < !-- --> 注釋會被 set 忽略決議以外,其它注釋例如 // 或 /**/ 或 -- 等都會被 set 標簽當成末尾子句元素處理,導致后續真正的末尾子句元素的逗號沒能被成功替換掉后綴,從而引起語法錯誤!

到此,我們的 where 標簽元素與 set 標簽就基本介紹完成,它倆確實極為相似,區別僅在于:

  • where 標簽插入前綴 where
  • set 標簽插入前綴 set
  • where 標簽僅智能替換前綴 AND 或 OR
  • set 標簽可以只能替換前綴逗號,或后綴逗號,

而這兩者的前后綴去除策略,都源自于 trim 標簽的設計,我們一起看看到底 trim 標簽是有多靈活!


Top5、trim 標簽

常用度:★☆☆☆☆

實用性:★☆☆☆☆

上面我們介紹了 where 標簽與 set 標簽,它倆的共同點無非就是前置關鍵詞 where 或 set 的插入,以及前后綴符號(例如 AND | OR | ,)的智能去除,基于 where 標簽和 set 標簽本身都繼承了 trim 標簽,所以 trim 標簽的大致實作我們也能猜出個一二三,

其實 where 標簽和 set 標簽都只是 trim 標簽的某種實作方案,trim 標簽底層是通過 TrimSqlNode 類來實作的,它有幾個關鍵屬性:

  • prefix :前綴,當 trim 元素記憶體在內容時,會給內容插入指定前綴
  • suffix :后綴,當 trim 元素記憶體在內容時,會給內容插入指定后綴
  • prefixesToOverride :前綴去除,支持多個,當 trim 元素記憶體在內容時,會把內容中匹配的前綴字串去除,
  • suffixesToOverride :后綴去除,支持多個,當 trim 元素記憶體在內容時,會把內容中匹配的后綴字串去除,

所以 where 標簽如果通過 trim 標簽實作的話可以這么撰寫:(

<!--
  注意在使用 trim 標簽實作 where 標簽能力時
  必須在 AND 和 OR 之后添加空格
  避免匹配到 android、order 等單詞 
-->
<trim prefix="WHERE" prefixOverrides="AND | OR" >
    ...
</trim>

而 set 標簽如果通過 trim 標簽實作的話可以這么撰寫:

<trim prefix="SET" prefixOverrides="," >
    ...
</trim>

或者

<trim prefix="SET" suffixesToOverride="," >
    ...
</trim>

所以可見 trim 是足夠靈活的,不過由于 where 標簽和 set 標簽這兩種 trim 標簽變種方案已經足以滿足我們實際開發需求,所以直接使用 trim 標簽的場景實際上不太很多(其實是我自己使用的不多,基本沒用過),

注意,set 標簽之所以能夠支持去除前綴逗號或者后綴逗號,是由于其在構造 trim 標簽的時候進行了前綴后綴的去除設定,而 where 標簽在構造 trim 標簽的時候就僅僅設定了前綴去除,

set 標簽元素之構造時:

// Set 標簽
public class SetSqlNode extends TrimSqlNode {

  private static final List<String> COMMA = Collections.singletonList(",");

  // 明顯使用了前綴后綴去除,注意前后綴引數都傳入了 COMMA 
  public SetSqlNode(Configuration configuration,SqlNode contents) {
    super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}

where 標簽元素之構造時:

// Where 標簽
public class WhereSqlNode extends TrimSqlNode {

  // 其實包含了很多種場景
  private static List<String> prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  // 明顯只使用了前綴去除,注意前綴傳入 prefixList,后綴傳入 null 
  public WhereSqlNode(Configuration configuration, SqlNode contents) {
    super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

Top6、bind 標簽

常用度:☆☆☆☆☆

實用性:★☆☆☆☆

簡單來說,這個標簽就是可以創建一個變數,并系結到背景關系,即供背景關系使用,就是這樣,我把官網的例子直接拷貝過來:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

大家應該大致能知道以上例子的功效,其實就是輔助構建模糊查詢的陳述句拼接,那有人就好奇了,為啥不直接拼接陳述句就行了,為什么還要搞出一個變數,繞一圈呢?

我先問一個問題:平時你使用 mysql 都是如何拼接模糊查詢 like 陳述句的?

select * from user where name like concat('%',#{name},'%')

確實如此,但如果有一天領導跟你說資料庫換成 oracle 了,怎么辦?上面的陳述句還能用嗎?明顯用不了,不能這么寫,因為 oracle 雖然也有 concat 函式,但是只支持連接兩個字串,例如你最多這么寫:

select * from user where name like concat('%',#{name})

但是少了右邊的井號符號,所以達不到你預期的效果,于是你改成這樣:

select * from user where name like '%'||#{name}||'%'

確實可以了,但是過幾天領導又跟你說,資料庫換回 mysql 了?額… 那不好意思,你又得把相關使用到模糊查詢的地方改回來,

select * from user where name like concat('%',#{name},'%')

很顯然,資料庫只要發生變更你的 sql 陳述句就得跟著改,特別麻煩,所以才有了一開始我們介紹 bind 標簽官網的這個例子,無論使用哪種資料庫,這個模糊查詢的 Like 語法都是支持的:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

這個 bind 的用法,實打實解決了資料庫重新選型后導致的一些問題,當然在實際作業中發生的概率不會太大,所以 bind 的使用我個人確實也使用的不多,可能還有其它一些應用場景,希望有人能發現之后來跟我們分享一下,總之我勉強給了一顆星(雖然沒太多實際用處,但畢竟要給點面子),


拓展:sql標簽 + include 標簽

常用度:★★★☆☆

實用性:★★★☆☆

sql 標簽與 include 標簽組合使用,用于 SQL 陳述句的復用,日常高頻或公用使用的陳述句塊可以抽取出來進行復用,其實我們應該不陌生,早期我們學習 JSP 的時候,就有一個 include 標記可以引入一些公用可復用的頁面檔案,例如頁面頭部或尾部頁面代碼元素,這種復用的設計很常見,

嚴格意義上 sql 、include 不算在動態 SQL 標簽成員之內,只因它確實是寶藏般的存在,所以我要簡單說說,sql 標簽用于定義一段可重用的 SQL 陳述句片段,以便在其它陳述句中使用,而 include 標簽則通過屬性 refid 來參考對應 id 匹配的 sql 標簽陳述句片段,

簡單的復用代碼塊可以是:

<!-- 可復用的欄位陳述句塊 -->
<sql id="userColumns">
    id,username,password 
</sql>

查詢或插入時簡單復用:

<!-- 查詢時簡單復用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"></include> 
  from user 
</select>

<!-- 插入時簡單復用 -->
<insert id="insertUser" resultType="map">
  insert into user(
    <include refid="userColumns"></include> 
  )values(
    #{id},#{username},#{password} 
  )  
</insert>

當然,復用陳述句還支持屬性傳遞,例如:

<!-- 可復用的欄位陳述句塊 -->
<sql id="userColumns">
    ${pojo}.id,${pojo}.username 
</sql>

這個 SQL 片段可以在其它陳述句中使用:

<!-- 查詢時復用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns">
        <property name="pojo" value="https://www.cnblogs.com/panshenlian/p/u1"/>
    </include>,
    <include refid="userColumns">
        <property name="pojo" value="https://www.cnblogs.com/panshenlian/p/u2"/>
    </include>
  from user u1 cross join user u2
</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, username
  <include refid="sql2">
    <property name="prefix" value="https://www.cnblogs.com/panshenlian/p/t"/>
    <property name="include_target" value="https://www.cnblogs.com/panshenlian/p/sql1"/>
  </include>
</select>

至此,關于 9 大動態 SQL 標簽的基本用法我們已介紹完畢,另外我們還有一些疑問:Mybatis 底層是如何決議這些動態 SQL 標簽的呢?最終又是怎么構建完整可執行的 SQL 陳述句的呢?帶著這些疑問,我們在第4節中詳細分析,

4、動態SQL的底層原理

想了解 Mybatis 究竟是如何決議與構建動態 SQL ?首先推薦的當然是讀原始碼,而讀原始碼,是一個技術鉆研問題,為了借鑒學習,為了作業儲備,為了解決問題,為了讓自己在編程的道路上跑得明白一些... 而希望通過讀原始碼,去了解底層實作原理,切記不能脫離了整體去讀區域,否則你了解到的必然局限且片面,從而輕忽了真核上的設計,如同我們讀史或者觀宇宙一樣,最好的辦法都是從整體到區域,不斷放大,前后延展,會很舒服通透,所以我準備從 Mybatis 框架的核心主線上去逐步放大剖析,

通過前面幾篇文章的介紹(建議閱讀 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道幾個?》),其實我們知道了 Mybatis 框架的核心部分在于構件的構建程序,從而支撐了外部應用程式的使用,從應用程式端創建配置并呼叫 API 開始,到框架端加載配置并初始化構件,再創建會話并接收請求,然后處理請求,最侄訓傳處理結果等,

我們的動態 SQL 決議部分就發生在 SQL 陳述句物件 MappedStatement 構建時(上左高亮橘色部分,注意觀察其中 SQL 陳述句物件與 SqlSource 、 BoundSql 的關系,在動態 SQL 決議流程特別關鍵),我們再拉近一點,可以看到無論是使用 XML 配置 SQL 陳述句或是使用注解方式配置 SQL 陳述句,框架最終都會把決議完成的 SQL 陳述句物件存放到 MappedStatement 陳述句集合池子,

而以上虛線高亮部分,即是 XML 配置方式決議程序與注解配置方式決議程序中涉及到動態 SQL 標簽決議的流程,我們分別講解:

  • 第一,XML 方式配置 SQL 陳述句,框架如何決議?

以上為 XML 配置方式的 SQL 陳述句決議程序,無論是單獨使用 Mybatis 框架還是集成 Spring 與 Mybatis 框架,程式啟動入口都會首先從 SqlSessionFactoryBuilder.build() 開始構建,依次通過 XMLConfigBuilder 構建全域配置 Configuration 物件、通過 XMLMapperBuilder 構建每一個 Mapper 映射器、通過 XMLStatementBuilder 構建映射器中的每一個 SQL 陳述句物件(select/insert/update/delete),而就在決議構建每一個 SQL 陳述句物件時,涉及到一個關鍵的方法 parseStatementNode(),即上圖橘紅色高亮部分,此方法內部就出現了一個處理動態 SQL 的核心節點,

// XML配置陳述句構建器
public class XMLStatementBuilder {
    
    // 實際決議每一個 SQL 陳述句
    // 例如 select|insert|update|delete
	public void parseStatementNode() {
        
        // [忽略]引數構建...
        // [忽略]快取構建..
        // [忽略]結果集構建等等.. 
        
        // 【重點】此處即是處理動態 SQL 的核心!!!
        String lang = context.getStringAttribute("lang");
    	LanguageDriver langDriver = getLanguageDriver(lang);
        SqlSource sqlSource = langDriver.createSqlSource(..);
        
        // [忽略]最后把決議完成的陳述句物件添加進陳述句集合池
        builderAssistant.addMappedStatement(陳述句物件)
	}
}

大家先重點關注一下這段代碼,其中【重點】部分的 LanguageDriver 與 SqlSource 會是我們接下來講解動態 SQL 陳述句決議的核心類,我們不著急剖析,我們先把注解方式流程也梳理對比一下,

  • 第二,注解方式配置 SQL 陳述句,框架如何決議?

大家會發現注解配置方式的 SQL 陳述句決議程序,與 XML 方式極為相像,唯一不同點就在于決議注解 SQL 陳述句時,使用了 MapperAnnotationBuilder 構建器,其中關于每一個陳述句物件 (@Select,@Insert,@Update,@Delete等) 的決議,又都會通過一個關鍵決議方法 parseStatement(),即上圖橘紅色高亮部分,此方法內部同樣的出現了一個處理動態 SQL 的核心節點,

// 注解配置陳述句構建器
public class MapperAnnotationBuilder {
    
    // 實際決議每一個 SQL 陳述句
    // 例如 @Select,@Insert,@Update,@Delete
    void parseStatement(Method method) {  
        
        // [忽略]引數構建...
        // [忽略]快取構建..
        // [忽略]結果集構建等等.. 
        
        // 【重點】此處即是處理動態 SQL 的核心!!!
    	final LanguageDriver languageDriver = getLanguageDriver(method);  
    	final SqlSource sqlSource = buildSqlSource( languageDriver,... );
        
        // [忽略]最后把決議完成的陳述句物件添加進陳述句集合池
        builderAssistant.addMappedStatement(陳述句物件)

    }    
}

由此可見,不管是通過 XML 配置陳述句還是注解方式配置陳述句,構建流程都是 大致相同,并且依然出現了我們在 XML 配置方式中涉及到的語言驅動 LanguageDriver 與陳述句源 SqlSource ,那這兩個類/介面到底為何物,為何能讓 SQL 陳述句決議者都如此繞不開 ?

這一切,得從你撰寫的 SQL 開始講起 ...

我們知道,無論 XML 還是注解,最終你的所有 SQL 陳述句物件都會被齊齊整整的決議完放置在 SQL 陳述句物件集合池中,以供執行器 Executor 具體執行增刪改查 ( CRUD ) 時使用,而我們知道每一個 SQL 陳述句物件的屬性,特別復雜繁多,例如超時設定、快取、陳述句型別、結果集映射關系等等,

// SQL 陳述句物件
public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
    
  // SQL 源
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;
    
}

而其中有一個特別的屬性就是我們的陳述句源 SqlSource ,功能純粹也恰如其名 SQL 源,它是一個介面,它會結合用戶傳遞的引數物件 parameterObject 與動態 SQL,生成 SQL 陳述句,并最終封裝成 BoundSql 物件,SqlSource 介面有5個實作類,分別是:StaticSqlSource、DynamicSqlSource、RawSqlSource、ProviderSqlSource、VelocitySqlSource (而 velocitySqlSource 目前只是一個測驗用例,還沒有用作實際的 Sql 源實作),

  • StaticSqlSource:靜態 SQL 源實作類,所有的 SQL 源最終都會構建成 StaticSqlSource 實體,該實作類會生成最終可執行的 SQL 陳述句供 statement 或 prepareStatement 使用,
  • RawSqlSource:原生 SQL 源實作類,決議構建含有 ‘#{}’ 占位符的 SQL 陳述句或原生 SQL 陳述句,決議完最侄訓構建 StaticSqlSource 實體,
  • DynamicSqlSource:動態 SQL 源實作類,決議構建含有 ‘${}’ 替換符的 SQL 陳述句或含有動態 SQL 的陳述句(例如 If/Where/Foreach等),決議完最侄訓構建 StaticSqlSource 實體,
  • ProviderSqlSource:注解方式的 SQL 源實作類,會根據 SQL 陳述句的內容分發給 RawSqlSource 或 DynamicSqlSource ,當然最終也會構建 StaticSqlSource 實體,
  • VelocitySqlSource:模板 SQL 源實作類,目前(V3.5.6)官方申明這只是一個測驗用例,還沒有用作真正的模板 Sql 源實作類,

SqlSource 實體在配置類 Configuration 決議階段就被創建,Mybatis 框架會依據3個維度的資訊來選擇構建哪種資料源實體:(純屬我個人理解的歸類梳理~)

  • 第一個維度:客戶端的 SQL 配置方式:XML 方式或者注解方式,
  • 第二個維度:SQL 陳述句中是否使用動態 SQL ( if/where/foreach 等 ),
  • 第三個維度:SQL 陳述句中是否含有替換符 ‘${}’ 或占位符 ‘#{}’ ,

SqlSource 介面只有一個方法 getBoundSql ,就是創建 BoundSql 物件,

public interface SqlSource {

  BoundSql getBoundSql(Object parameterObject);

}

通過 SQL 源就能夠獲取 BoundSql 物件,從而獲取最終送往資料庫(通過JDBC)中執行的 SQL 字串,

JDBC 中執行的 SQL 字串,確實就在 BoundSql 物件中,BoundSql 物件存盤了動態(或靜態)生成的 SQL 陳述句以及相應的引數資訊,它是在執行器具體執行 CURD 時通過實際的 SqlSource 實體所構建的,

public class BoundSql { 

  //該欄位中記錄了SQL陳述句,該SQL陳述句中可能含有"?"占位符
  private final String sql;
    
  //SQL中的引數屬性集合
  private final List<ParameterMapping> parameterMappings;
    
  //客戶端執行SQL時傳入的實際引數值
  private final Object parameterObject;
    
  //復制 DynamicContext.bindings 集合中的內容
  private final Map<String, Object> additionalParameters;
    
  //通過 additionalParameters 構建元引數物件
  private final MetaObject metaParameters;
    
}

在執行器 Executor 實體(例如BaseExecutor)執行增刪改查時,會通過 SqlSource 構建 BoundSql 實體,然后再通過 BoundSql 實體獲取最終輸送至資料庫執行的 SQL 陳述句,系統可根據 SQL 陳述句構建 Statement 或者 PrepareStatement ,從而送往資料庫執行,例如陳述句處理器 StatementHandler 的執行程序,

墻裂推薦閱讀之前第六文之 Mybatis 最硬核的 API 你知道幾個?這些執行流程都有細講,

到此我們介紹完 SQL 源 SqlSource 與 BoundSql 的關系,注意 SqlSource 與 BoundSql 不是同個階段產生的,而是分別在程式啟動階段與運行時,

  • 程式啟動初始構建時,框架會根據 SQL 陳述句型別構建對應的 SqlSource 源實體(靜態/動態).
  • 程式實際運行時,框架會根據傳入引數動態的構建 BoundSql 物件,輸送最終 SQL 到資料庫執行,

在上面我們知道了 SQL 源是陳述句物件 BoundSql 的屬性,同時還坐擁5大實作類,那究竟是誰創建了 SQL 源呢?其實就是我們接下來準備介紹的語言驅動 LanguageDriver !

public interface LanguageDriver {
    SqlSource createSqlSource(...);
}

語言驅動介面 LanguageDriver 也是極簡潔,內部定義了構建 SQL 源的方法,LanguageDriver 介面有2個實作類,分別是: XMLLanguageDriver 、 RawLanguageDriver,簡單介紹一下:

  • XMLLanguageDriver :是框架默認的語言驅動,能夠根據上面我們講解的 SQL 源的3個維度創建對應匹配的 SQL 源(DynamicSqlSource、RawSqlSource等),下面這段代碼是 Mybatis 在裝配全域配置時的一些跟語言驅動相關的動作,我摘抄出來,分別有:內置了兩種語言驅動并設定了別名方便參考、注冊了兩種語言驅動至語言注冊工廠、把 XML 語言驅動設定為默認語言驅動,
// 全域配置的構造方法
public Configuration() {
    // 內置/注冊了很多有意思的【別名】
    // ...
    
    // 其中就內置了上述的兩種語言驅動【別名】
    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    
    // 注冊了XML【語言驅動】 --> 并設定成默認!   
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    
    // 注冊了原生【語言驅動】
    languageRegistry.register(RawLanguageDriver.class);
}
  • RawLanguageDriver :看名字得知是原生語言驅動,事實也如此,它只能創建原生 SQL 源(RawSqlSource),另外它還繼承了 XMLLanguageDriver ,
/**
 * As of 3.2.4 the default XML language is able to identify static statements
 * and create a {@link RawSqlSource}. So there is no need to use RAW unless you
 * want to make sure that there is not any dynamic tag for any reason.
 *
 * @since 3.2.0
 * @author Eduardo Macarron
 */
public class RawLanguageDriver extends XMLLanguageDriver {
}

注釋的大致意思:自 Mybatis 3.2.4 之后的版本, XML 語言驅動就支持決議靜態陳述句(動態陳述句當然也支持)并創建對應的 SQL 源(例如靜態陳述句是原生 SQL 源),所以除非你十分確定你的 SQL 陳述句中沒有包含任何一款動態標簽,否則就不要使用 RawLanguageDriver !否則會報錯!!!先看個別名參考的例子:

<select id="findAll"  resultType="map" lang="RAW" >
     select * from user
</select>

<!-- 別名或全限定類名都允許 -->

<select id="findAll"  resultType="map" lang="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver">
     select * from user
</select>

框架允許我們通過 lang 屬性手工指定語言驅動,不指定則系統默認是 lang = "XML",XML 代表 XMLLanguageDriver ,當然 lang 屬性可以是我們內置的別名也可以是我們的語言驅動全限定名,不過值得注意的是,當陳述句中含有動態 SQL 標簽時,就只能選擇使用 lang="XML",否則程式在初始化構件時就會報錯,

## Cause: org.apache.ibatis.builder.BuilderException: 
## Dynamic content is not allowed when using RAW language
## 動態陳述句內容不被原生語言驅動支持!

這段錯誤提示其實是發生在 RawLanguageDriver 檢查動態 SQL 源時:

public class RawLanguageDriver extends XMLLanguageDriver { 

  // RAW 不能包含動態內容
  private void checkIsNotDynamic(SqlSource source) {
    if (!RawSqlSource.class.equals(source.getClass())) {
      throw new BuilderException(
          "Dynamic content is not allowed when using RAW language"
      );
    }
  } 
}

至此,基本邏輯我們已經梳理清楚:程式啟動初始階段,語言驅動創建 SQL 源,而運行時, SQL 源動態決議構建出 BoundSql ,

那么除了系統默認的兩種語言驅動,還有其它嗎?

答案是:有,例如 Mybatis 框架中目前使用了一個名為 VelocityLanguageDriver 的語言驅動,相信大家都學習過 JSP 模板引擎,同時還有很多人學習過其它一些(頁面)模板引擎,例如 freemark 和 velocity ,不同模板引擎有自己的一套模板語言語法,而其中 Mybatis 就嘗試使用了 Velocity 模板引擎作為語言驅動,目前雖然 Mybatis 只是在測驗用例中使用到,但是它告訴了我們,框架允許自定義語言驅動,所以不只是 XML、RAW 兩種語言驅動中使用的 OGNL 語法,也可以是 Velocity (語法),或者你自己所能定義的一套模板語言(同時你得定義一套語法), 例如以下就是 Mybatis 框架中使用到的 Velocity 語言驅動和對應的 SQL 源,它們使用 Velocity 語法/方式決議構建 BoundSql 物件,

/**
 * Just a test case. Not a real Velocity implementation.
 * 只是一個測驗示例,還不是一個真正的 Velocity 方式實作
 */
public class VelocityLanguageDriver implements LanguageDriver {
     public SqlSource createSqlSource() {...}
}
public class VelocitySqlSource implements SqlSource {
     public BoundSql getBoundSql() {...}
}

好,語言驅動的基本概念大致如此,我們回過頭再詳細看看動態 SQL 源 SqlSource,作為陳述句物件 MappedStatement 的屬性,在 程式初始構建階段,語言驅動是怎么創建它的呢?不妨我們先看看常用的動態 SQL 源物件是怎么被創建的吧!

通過以上的程式初始構建階段,我們可以發現,最終語言驅動通過呼叫 XMLScriptBuilder 物件來創建 SQL 源,

// XML 語言驅動
public class XMLLanguageDriver implements LanguageDriver {  
  
    // 通過呼叫 XMLScriptBuilder 物件來創建 SQL 源
    @Override
  	public SqlSource createSqlSource() {
        // 實體
    	XMLScriptBuilder builder = new XMLScriptBuilder();
    	// 決議
        return builder.parseScriptNode();
	}
}

而在前面我們就已經介紹, XMLScriptBuilder 實體初始構造時,會初始構建所有動態標簽處理器:

// XML腳本標簽構建器
public class XMLScriptBuilder{
    // 標簽節點處理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    // 構造器
    public XMLScriptBuilder() { 
        initNodeHandlerMap();
        //... 其它初始化不贅述也不重要
    }

    // 動態標簽處理器
    private void initNodeHandlerMap() {
        nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
}

繼 XMLScriptBuilder 初始化流程之后,決議創建 SQL 源流程再分為兩步:

1、決議動態標簽,通過判斷每一塊動態標簽的型別,使用對應的標簽處理器進行決議屬性和陳述句處理,并最終放置到混合 SQL 節點池中(MixedSqlNode),以供程式運行時構建 BoundSql 時使用,

2、new SQL 源,根據 SQL 是否有動態標簽或通配符占位符來確認產生物件的靜態或動態 SQL 源,

public SqlSource parseScriptNode() {
    
    // 1、決議動態標簽 ,并放到混合SQL節點池中
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    
    // 2、根據陳述句型別,new 出來最終的 SQL 源
    SqlSource sqlSource;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

原來決議動態標簽的作業交給了 parseDynamicTags() 方法,并且每一個陳述句物件的動態 SQL 標簽最終都會被放到一個混合 SQL 節點池中,

// 混合 SQL 節點池
public class MixedSqlNode implements SqlNode {
    
    // 所有動態 SQL 標簽:IF、WHERE、SET 等
    private final List<SqlNode> contents;
}

我們先看一下 SqlNode 介面的實作類,基本涵蓋了我們所有動態 SQL 標簽處理器所需要使用到的節點實體,而其中混合 SQL 節點 MixedSqlNode 作用僅是為了方便獲取每一個陳述句的所有動態標簽節點,于是應勢而生,

知道動態 SQL 標簽節點處理器及以上的節點實作類之后,其實就能很容易理解,到達程式運行時,執行器會呼叫 SQL 源來協助構建 BoundSql 物件,而 SQL 源的核心作業,就是根據每一小段標簽型別,匹配到對應的節點實作類以決議拼接每一小段 SQL 陳述句,

程式運行時,動態 SQL 源獲取 BoundSql 物件 :

// 動態 SQL 源
public class DynamicSqlSource implements SqlSource { 
   
    // 這里的 rootSqlNode 屬性就是 MixedSqlNode 
    private final SqlNode rootSqlNode;
  
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
 
        // 動態SQL核心決議流程  
        rootSqlNode.apply(...);  
        
        return boundSql;

    } 
}

很明顯,通過呼叫 MixedSqlNode 的 apply () 方法,回圈遍歷每一個具體的標簽節點,

public class MixedSqlNode implements SqlNode {
    
      // 所有動態 SQL 標簽:IF、WHERE、SET 等
      private final List<SqlNode> contents; 

      @Override
      public boolean apply(...) {

        // 回圈遍歷,把每一個節點的決議分派到具體的節點實作之上
        // 例如 <if> 節點的決議交給 IfSqlNode
        // 例如 純文本節點的決議交給 StaticTextSqlNode
        contents.forEach(node -> node.apply(...));
        return true;
      }
}

我們選擇一兩個標簽節點的決議程序進行說明,其它標簽節點實作類的處理也基本雷同,首先我們看一下 IF 標簽節點的處理:

// IF 標簽節點
public class IfSqlNode implements SqlNode { 
    
      private final ExpressionEvaluator evaluator;
    
      // 實作邏輯
      @Override
      public boolean apply(DynamicContext context) {
          
        // evaluator 是一個基于 OGNL 語法的決議校驗類
        if (evaluator.evaluateBoolean(test, context.getBindings())) {
          contents.apply(context);
          return true;
        }
        return false;
      } 
}

IF 標簽節點的決議程序非常簡單,通過決議校驗類 ExpressionEvaluator 來對 IF 標簽的 test 屬性內的運算式進行決議校驗,滿足則拼接,不滿足則跳過,我們再看看 Trim 標簽的節點決議程序,set 標簽與 where 標簽的底層處理都基于此:

public class TrimSqlNode implements SqlNode { 
    
    // 核心處理方法
    public void applyAll() {
        
        // 前綴智能補充與去除
        applyPrefix(..); 
        
        // 前綴智能補充與去除
        applySuffix(..); 
    } 
}

再來看一個純文本標簽節點實作類的決議處理流程:

// 純文本標簽節點實作類
public class StaticTextSqlNode implements SqlNode {
  
    private final String text;

    public StaticTextSqlNode(String text) {
        this.text = text;
    }
    
    // 節點處理,僅僅就是純粹的陳述句拼接
    @Override
    public boolean apply(DynamicContext context) {
        context.appendSql(text);
        return true;
  	}
}

到這里,動態 SQL 的底層決議程序我們基本講解完,冗長了些,但流程上大致算完整,有遺漏的,我們回頭再補充,

總結

不知不覺中,我又是這么巨篇幅的講解剖析,確實不太適合碎片化時間閱讀,不過話說回來,畢竟此文屬于 Mybatis 全解系列,作為學研者還是建議深諳其中,對往后眾多框架技術的學習必有幫助,本文中我們很多動態 SQL 的介紹基本都使用 XML 配置方式,當然注解方式配置動態 SQL 也是支持的,動態 SQL 的語法書寫同 XML 方式,但是需要在字串前后添加 script 標簽申明該陳述句為動態 SQL ,例如:

public class UserDao {
   
    /**
     * 更新用戶
     */
    @Select(
        "<script>"+
        "   UPDATE user   "+
        "   <trim prefix=\"SET\" prefixOverrides=\",\"> "+
        "       <if test=\"username != null and username != ''\"> "+
        "           , username = #{username} "+
        "       </if> "+
        "   </trim> "+
        "   where id = ${id}"
        "</script>"
    )
    void updateUser( User user);
    
}

此種動態 SQL 寫法可讀性較差,并且維護起來也挺硌手,所以我個人是青睞 xml 方式配置陳述句,一直追求解耦,大道也至簡,當然,也有很多團隊和專案都在使用注解方式開發,這些沒有絕對,還是得結合自己的實際專案情況與團隊等去做取舍,

本篇完,本系列下一篇我們講《 Mybatis系列全解(九):Mybatis的復雜映射 》,

文章持續更新,微信搜索「潘潘和他的朋友們」第一時間閱讀,隨時有驚喜,本文會在 GitHub https://github.com/JavaWorld 收錄,關于熱騰騰的技術、框架、面經、解決方案、摸魚技巧、教程、視頻、漫畫等等等等,我們都會以最美的姿勢第一時間送達,歡迎 Star ~ 我們未來 不止文章!想進讀者群的朋友歡迎撩我個人號:panshenlian,備注「加群」我們群里暢聊, BIU ~

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/266210.html

標籤:Java

上一篇:Java this 的使用問題

下一篇:MySQL時間戳unix_timestamp

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more