MyBatis-動態SQL使用和原理


參考:

https://www.cnblogs.com/ysocean/p/7289529.html

https://www.cnblogs.com/fangjian0423/p/mybaits-dynamic-sql-analysis.html

 

 

 

 

mybatis 詳解(五)------動態SQL

 


  前面幾篇博客我們通過實例講解了用mybatis對一張表進行的CRUD操作,但是我們發現寫的 SQL 語句都比較簡單,如果有比較復雜的業務,我們需要寫復雜的 SQL 語句,往往需要拼接,而拼接 SQL ,稍微不注意,由於引號,空格等缺失可能都會導致錯誤。

  那么怎么去解決這個問題呢?這就是本篇所講的使用 mybatis 動態SQL,通過 if, choose, when, otherwise, trim, where, set, foreach等標簽,可組合成非常靈活的SQL語句,從而在提高 SQL 語句的准確性的同時,也大大提高了開發人員的效率。

 

  我們以 User 表為例來說明:

  

1、動態SQL:if 語句

  根據 username 和 sex 來查詢數據。如果username為空,那么將只根據sex來查詢;反之只根據username來查詢

  首先不使用 動態SQL 來書寫

1
2
3
4
5
6
<select id= "selectUserByUsernameAndSex"
         resultType= "user"  parameterType= "com.ys.po.User" >
     <!-- 這里和普通的sql 查詢語句差不多,對於只有一個參數,后面的 #{id}表示占位符,里面不一定要寫id,
             寫啥都可以,但是不要空着,如果有多個參數則必須寫pojo類里面的屬性 -->
     select * from user where username=#{username} and sex=#{sex}
</select>

  

  上面的查詢語句,我們可以發現,如果 #{username} 為空,那么查詢結果也是空,如何解決這個問題呢?使用 if 來判斷

1
2
3
4
5
6
7
8
9
10
<select id= "selectUserByUsernameAndSex"  resultType= "user"  parameterType= "com.ys.po.User" >
     select * from user where
         < if  test= "username != null" >
            username=#{username}
         </ if >
         
         < if  test= "username != null" >
            and sex=#{sex}
         </ if >
</select>

  這樣寫我們可以看到,如果 sex 等於 null,那么查詢語句為 select * from user where username=#{username},但是如果usename 為空呢?那么查詢語句為 select * from user where and sex=#{sex},這是錯誤的 SQL 語句,如何解決呢?請看下面的 where 語句

 

2、動態SQL:if+where 語句

1
2
3
4
5
6
7
8
9
10
11
12
<select id= "selectUserByUsernameAndSex"  resultType= "user"  parameterType= "com.ys.po.User" >
     select * from user
     <where>
         < if  test= "username != null" >
            username=#{username}
         </ if >
         
         < if  test= "username != null" >
            and sex=#{sex}
         </ if >
     </where>
</select>

  這個“where”標簽會知道如果它包含的標簽中有返回值的話,它就插入一個‘where’。此外,如果標簽返回的內容是以AND 或OR 開頭的,則它會剔除掉。

  

3、動態SQL:if+set 語句

  同理,上面的對於查詢 SQL 語句包含 where 關鍵字,如果在進行更新操作的時候,含有 set 關鍵詞,我們怎么處理呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 根據 id 更新 user 表的數據 -->
<update id= "updateUserById"  parameterType= "com.ys.po.User" >
     update user u
         <set>
             < if  test= "username != null and username != ''" >
                 u.username = #{username},
             </ if >
             < if  test= "sex != null and sex != ''" >
                 u.sex = #{sex}
             </ if >
         </set>
     
      where id=#{id}
</update>

  這樣寫,如果第一個條件 username 為空,那么 sql 語句為:update user u set u.sex=? where id=?

      如果第一個條件不為空,那么 sql 語句為:update user u set u.username = ? ,u.sex = ? where id=?

4、動態SQL:choose(when,otherwise) 語句

  有時候,我們不想用到所有的查詢條件,只想選擇其中的一個,查詢條件有一個滿足即可,使用 choose 標簽可以解決此類問題,類似於 Java 的 switch 語句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id= "selectUserByChoose"  resultType= "com.ys.po.User"  parameterType= "com.ys.po.User" >
       select * from user
       <where>
           <choose>
               <when test= "id !='' and id != null" >
                   id=#{id}
               </when>
               <when test= "username !='' and username != null" >
                   and username=#{username}
               </when>
               <otherwise>
                   and sex=#{sex}
               </otherwise>
           </choose>
       </where>
   </select>

  也就是說,這里我們有三個條件,id,username,sex,只能選擇一個作為查詢條件

    如果 id 不為空,那么查詢語句為:select * from user where  id=?

    如果 id 為空,那么看username 是否為空,如果不為空,那么語句為 select * from user where  username=?;

          如果 username 為空,那么查詢語句為 select * from user where sex=?

  

5、動態SQL:trim 語句

  trim標記是一個格式化的標記,可以完成set或者是where標記的功能

  ①、用 trim 改寫上面第二點的 if+where 語句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<select id= "selectUserByUsernameAndSex"  resultType= "user"  parameterType= "com.ys.po.User" >
         select * from user
         <!-- <where>
             < if  test= "username != null" >
                username=#{username}
             </ if >
             
             < if  test= "username != null" >
                and sex=#{sex}
             </ if >
         </where>  -->
         <trim prefix= "where"  prefixOverrides= "and | or" >
             < if  test= "username != null" >
                and username=#{username}
             </ if >
             < if  test= "sex != null" >
                and sex=#{sex}
             </ if >
         </trim>
     </select>

  prefix:前綴      

  prefixoverride:去掉第一個and或者是or

 

  ②、用 trim 改寫上面第三點的 if+set 語句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- 根據 id 更新 user 表的數據 -->
     <update id= "updateUserById"  parameterType= "com.ys.po.User" >
         update user u
             <!-- <set>
                 < if  test= "username != null and username != ''" >
                     u.username = #{username},
                 </ if >
                 < if  test= "sex != null and sex != ''" >
                     u.sex = #{sex}
                 </ if >
             </set> -->
             <trim prefix= "set"  suffixOverrides= "," >
                 < if  test= "username != null and username != ''" >
                     u.username = #{username},
                 </ if >
                 < if  test= "sex != null and sex != ''" >
                     u.sex = #{sex},
                 </ if >
             </trim>
         
          where id=#{id}
     </update>

  suffix:后綴  

  suffixoverride:去掉最后一個逗號(也可以是其他的標記,就像是上面前綴中的and一樣)

 

 

6、動態SQL: SQL 片段

  有時候可能某個 sql 語句我們用的特別多,為了增加代碼的重用性,簡化代碼,我們需要將這些代碼抽取出來,然后使用時直接調用。

  比如:假如我們需要經常根據用戶名和性別來進行聯合查詢,那么我們就把這個代碼抽取出來,如下:

1
2
3
4
5
6
7
8
9
<!-- 定義 sql 片段 -->
<sql id= "selectUserByUserNameAndSexSQL" >
     < if  test= "username != null and username != ''" >
         AND username = #{username}
     </ if >
     < if  test= "sex != null and sex != ''" >
         AND sex = #{sex}
     </ if >
</sql>

  引用 sql 片段

1
2
3
4
5
6
7
8
<select id= "selectUserByUsernameAndSex"  resultType= "user"  parameterType= "com.ys.po.User" >
     select * from user
     <trim prefix= "where"  prefixOverrides= "and | or" >
         <!-- 引用 sql 片段,如果refid 指定的不在本文件中,那么需要在前面加上 namespace -->
         <include refid= "selectUserByUserNameAndSexSQL" ></include>
         <!-- 在這里還可以引用其他的 sql 片段 -->
     </trim>
</select>

  注意:①、最好基於 單表來定義 sql 片段,提高片段的可重用性

     ②、在 sql 片段中最好不要包括 where 

    

7、動態SQL: foreach 語句

  需求:我們需要查詢 user 表中 id 分別為1,2,3的用戶

  sql語句:select * from user where id=1 or id=2 or id=3

       select * from user where id in (1,2,3)

 

①、建立一個 UserVo 類,里面封裝一個 List<Integer> ids 的屬性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package  com.ys.vo;
 
import  java.util.List;
 
public  class  UserVo {
     //封裝多個用戶的id
     private  List<Integer> ids;
 
     public  List<Integer> getIds() {
         return  ids;
     }
 
     public  void  setIds(List<Integer> ids) {
         this .ids = ids;
     }
 
}  

 

②、我們用 foreach 來改寫 select * from user where id=1 or id=2 or id=3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id= "selectUserByListId"  parameterType= "com.ys.vo.UserVo"  resultType= "com.ys.po.User" >
     select * from user
     <where>
         <!--
             collection:指定輸入對象中的集合屬性
             item:每次遍歷生成的對象
             open:開始遍歷時的拼接字符串
             close:結束時拼接的字符串
             separator:遍歷對象之間需要拼接的字符串
             select * from user where  1 = 1  and (id= 1  or id= 2  or id= 3 )
           -->
         <foreach collection= "ids"  item= "id"  open= "and ("  close= ")"  separator= "or" >
             id=#{id}
         </foreach>
     </where>
</select>

  測試:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//根據id集合查詢user表數據
@Test
public  void  testSelectUserByListId(){
     String statement =  "com.ys.po.userMapper.selectUserByListId" ;
     UserVo uv =  new  UserVo();
     List<Integer> ids =  new  ArrayList<>();
     ids.add( 1 );
     ids.add( 2 );
     ids.add( 3 );
     uv.setIds(ids);
     List<User> listUser = session.selectList(statement, uv);
     for (User u : listUser){
         System.out.println(u);
     }
     session.close();
}

  

③、我們用 foreach 來改寫 select * from user where id in (1,2,3)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<select id= "selectUserByListId"  parameterType= "com.ys.vo.UserVo"  resultType= "com.ys.po.User" >
         select * from user
         <where>
             <!--
                 collection:指定輸入對象中的集合屬性
                 item:每次遍歷生成的對象
                 open:開始遍歷時的拼接字符串
                 close:結束時拼接的字符串
                 separator:遍歷對象之間需要拼接的字符串
                 select * from user where  1 = 1  and id in ( 1 , 2 , 3 )
               -->
             <foreach collection= "ids"  item= "id"  open= "and id in ("  close= ") "  separator= "," >
                 #{id}
             </foreach>
         </where>
     </select>

  

8、總結

  其實動態 sql 語句的編寫往往就是一個拼接的問題,為了保證拼接准確,我們最好首先要寫原生的 sql 語句出來,然后在通過 mybatis 動態sql 對照着改,防止出錯。

 

 

 

 

 

 

Mybatis解析動態sql原理分析

前言

廢話不多說,直接進入文章。
我們在使用mybatis的時候,會在xml中編寫sql語句。
比如這段動態sql代碼:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users <trim prefix="SET" prefixOverrides=","> <if test="name != null and name != ''"> name = #{name} </if> <if test="age != null and age != ''"> , age = #{age} </if> <if test="birthday != null and birthday != ''"> , birthday = #{birthday} </if> </trim> where id = ${id} </update> 

mybatis底層是如何構造這段sql的?
這方面的知識網上資料不多,於是就寫了這么一篇文章。
下面帶着這個疑問,我們一步一步分析。

介紹MyBatis中一些關於動態SQL的接口和類

SqlNode接口,簡單理解就是xml中的每個標簽,比如上述sql的update,trim,if標簽:

public interface SqlNode { boolean apply(DynamicContext context); } 

SqlSource Sql源接口,代表從xml文件或注解映射的sql內容,主要就是用於創建BoundSql,有實現類DynamicSqlSource(動態Sql源),StaticSqlSource(靜態Sql源)等:

public interface SqlSource { BoundSql getBoundSql(Object parameterObject); } 

BoundSql類,封裝mybatis最終產生sql的類,包括sql語句,參數,參數源數據等參數:

XNode,一個Dom API中的Node接口的擴展類。

BaseBuilder接口及其實現類(屬性,方法省略了,大家有興趣的自己看),這些Builder的作用就是用於構造sql:

下面我們簡單分析下其中4個Builder:

1 XMLConfigBuilder

解析mybatis中configLocation屬性中的全局xml文件,內部會使用XMLMapperBuilder解析各個xml文件。

2 XMLMapperBuilder

遍歷mybatis中mapperLocations屬性中的xml文件中每個節點的Builder,比如user.xml,內部會使用XMLStatementBuilder處理xml中的每個節點。

3 XMLStatementBuilder

解析xml文件中各個節點,比如select,insert,update,delete節點,內部會使用XMLScriptBuilder處理節點的sql部分,遍歷產生的數據會丟到Configuration的mappedStatements中。

4 XMLScriptBuilder

解析xml中各個節點sql部分的Builder。

LanguageDriver接口及其實現類(屬性,方法省略了,大家有興趣的自己看),該接口主要的作用就是構造sql:

簡單分析下XMLLanguageDriver(處理xml中的sql,RawLanguageDriver處理靜態sql):

XMLLanguageDriver內部會使用XMLScriptBuilder解析xml中的sql部分。

ok, 大部分比較重要的類我們都已經介紹了,下面源碼分析走起。

源碼分析走起

Spring與Mybatis整合的時候需要配置SqlSessionFactoryBean,該配置會加入數據源和mybatis xml配置文件路徑等信息:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatisConfig.xml"/> <property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/> </bean> 

我們就分析這一段配置背后的細節:

SqlSessionFactoryBean實現了Spring的InitializingBean接口,InitializingBean接口的afterPropertiesSet方法中會調用buildSqlSessionFactory方法

buildSqlSessionFactory方法內部會使用XMLConfigBuilder解析屬性configLocation中配置的路徑,還會使用XMLMapperBuilder屬性解析mapperLocations屬性中的各個xml文件。

部分源碼如下:

由於XMLConfigBuilder內部也是使用XMLMapperBuilder,我們就看看XMLMapperBuilder的解析細節。

我們關注一下,增刪改查節點的解析。

XMLStatementBuilder的解析:

默認會使用XMLLanguageDriver創建SqlSource(Configuration構造函數中設置)。

XMLLanguageDriver創建SqlSource:

XMLScriptBuilder解析sql:

得到SqlSource之后,會放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最終的sql。

實例分析

我以以下xml的解析大概說下parseDynamicTags的解析過程:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users <trim prefix="SET" prefixOverrides=","> <if test="name != null and name != ''"> name = #{name} </if> <if test="age != null and age != ''"> , age = #{age} </if> <if test="birthday != null and birthday != ''"> , birthday = #{birthday} </if> </trim> where id = ${id} </update> 

在看這段解析之前,請先了解dom相關的知識,xml dom知識dom博文

parseDynamicTags方法的返回值是一個List,也就是一個Sql節點集合。SqlNode本文一開始已經介紹,分析完解析過程之后會說一下各個SqlNode類型的作用。

1 首先根據update節點(Node)得到所有的子節點,分別是3個子節點

(1)文本節點 \n UPDATE users

(2)trim子節點 ...

(3)文本節點 \n where id = #{id}

2 遍歷各個子節點

(1) 如果節點類型是文本或者CDATA,構造一個TextSqlNode或StaticTextSqlNode

(2) 如果節點類型是元素,說明該update節點是個動態sql,然后會使用NodeHandler處理各個類型的子節點。這里的NodeHandler是XMLScriptBuilder的一個內部接口,其實現類包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看類名也就明白了這個Handler的作用,比如我們分析的trim節點,對應的是TrimHandler;if節點,對應的是IfHandler...

這里子節點trim被TrimHandler處理,TrimHandler內部也使用parseDynamicTags方法解析節點

3 遇到子節點是元素的話,重復以上步驟

trim子節點內部有7個子節點,分別是文本節點、if節點、是文本節點、if節點、是文本節點、if節點、文本節點。文本節點跟之前一樣處理,if節點使用IfHandler處理

遍歷步驟如上所示,下面我們看下幾個Handler的實現細節。

IfHandler處理方法也是使用parseDynamicTags方法,然后加上if標簽必要的屬性。

private class IfHandler implements NodeHandler { public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { List<SqlNode> contents = parseDynamicTags(nodeToHandle); MixedSqlNode mixedSqlNode = new MixedSqlNode(contents); String test = nodeToHandle.getStringAttribute("test"); IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test); targetContents.add(ifSqlNode); } } 

TrimHandler處理方法也是使用parseDynamicTags方法,然后加上trim標簽必要的屬性。

private class TrimHandler implements NodeHandler { public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) { List<SqlNode> contents = parseDynamicTags(nodeToHandle); MixedSqlNode mixedSqlNode = new MixedSqlNode(contents); String prefix = nodeToHandle.getStringAttribute("prefix"); String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides"); String suffix = nodeToHandle.getStringAttribute("suffix"); String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides"); TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides); targetContents.add(trim); } } 

以上update方法最終通過parseDynamicTags方法得到的SqlNode集合如下:

trim節點:

由於這個update方法是個動態節點,因此構造出了DynamicSqlSource。

DynamicSqlSource內部就可以構造sql了:

DynamicSqlSource內部的SqlNode屬性是一個MixedSqlNode。

然后我們看看各個SqlNode實現類的apply方法

下面分析一下兩個SqlNode實現類的apply方法實現:

MixedSqlNode:

public boolean apply(DynamicContext context) { for (SqlNode sqlNode : contents) { sqlNode.apply(context); } return true; } 

MixedSqlNode會遍歷調用內部各個sqlNode的apply方法。

StaticTextSqlNode:

public boolean apply(DynamicContext context) { context.appendSql(text); return true; } 

直接append sql文本。

IfSqlNode:

public boolean apply(DynamicContext context) { if (evaluator.evaluateBoolean(test, context.getBindings())) { contents.apply(context); return true; } return false; } 

這里的evaluator是一個ExpressionEvaluator類型的實例,內部使用了OGNL處理表達式邏輯。

TrimSqlNode:

public boolean apply(DynamicContext context) { FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); boolean result = contents.apply(filteredDynamicContext); filteredDynamicContext.applyAll(); return result; } public void applyAll() { sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); if (trimmedUppercaseSql.length() > 0) { applyPrefix(sqlBuffer, trimmedUppercaseSql); applySuffix(sqlBuffer, trimmedUppercaseSql); } delegate.appendSql(sqlBuffer.toString()); } private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { if (!prefixApplied) { prefixApplied = true; if (prefixesToOverride != null) { for (String toRemove : prefixesToOverride) { if (trimmedUppercaseSql.startsWith(toRemove)) { sql.delete(0, toRemove.trim().length()); break; } } } if (prefix != null) { sql.insert(0, " "); sql.insert(0, prefix); } } } 

TrimSqlNode的apply方法也是調用屬性contents(一般都是MixedSqlNode)的apply方法,按照實例也就是7個SqlNode,都是StaticTextSqlNode和IfSqlNode。 最后會使用FilteredDynamicContext過濾掉prefix和suffix。

總結

大致講解了一下mybatis對動態sql語句的解析過程,其實回過頭來看看不算復雜,還算蠻簡單的。 之前接觸mybaits的時候遇到剛才分析的那一段動態sql的時候總是很費解。

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users <trim prefix="SET" prefixOverrides=","> <if test="name != null and name != ''"> name = #{name} </if> <if test="age != null and age != ''"> , age = #{age} </if> <if test="birthday != null and birthday != ''"> , birthday = #{birthday} </if> </trim> where id = ${id} </update> 

想搞明白這個trim節點的prefixOverrides到底是什么意思(從字面上理解就是前綴覆蓋),而且官方文檔上也沒這方面知識的說明。我將這段xml改成如下:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User"> UPDATE users <trim prefix="SET" prefixOverrides=","> <if test="name != null and name != ''"> , name = #{name} </if> <if test="age != null and age != ''"> , age = #{age} </if> <if test="birthday != null and birthday != ''"> , birthday = #{birthday} </if> </trim> where id = ${id} </update> 

(第二段第一個if節點多了個逗號) 結果我發現這2段xml解析的結果是一樣的,非常迫切地想知道這到底是為什么,然后這也促使了我去看源碼的決心。最終還是看下來了。

 


免責聲明!

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



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