1. 程式人生 > 實用技巧 >MyBatis-動態SQL使用和原理

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 <iftest="username != null"> username=#{username} </if> <iftest="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> <iftest="username != null"> username=#{username} </if> <iftest="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> <iftest="username != null and username != ''"> u.username = #{username}, </if> <iftest="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 whereusername=?;

          如果 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> <iftest="username != null"> username=#{username} </if> <iftest="username != null"> and sex=#{sex} </if> </where> --> <trim prefix="where"prefixOverrides="and | or"> <iftest="username != null"> and username=#{username} </if> <iftest="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> <iftest="username != null and username != ''"> u.username = #{username}, </if> <iftest="sex != null and sex != ''"> u.sex = #{sex} </if> </set> --> <trim prefix="set"suffixOverrides=","> <iftest="username != null and username != ''"> u.username = #{username}, </if> <iftest="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"> <iftest="username != null and username != ''"> AND username = #{username} </if> <iftest="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 packagecom.ys.vo; importjava.util.List; publicclassUserVo { //封裝多個使用者的id privateList<Integer> ids; publicList<Integer> getIds() { returnids; } publicvoidsetIds(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 where1=1and (id=1or id=2or 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 publicvoidtestSelectUserByListId(){ String statement ="com.ys.po.userMapper.selectUserByListId"; UserVo uv =newUserVo(); List<Integer> ids =newArrayList<>(); 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 where1=1and 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解析的結果是一樣的,非常迫切地想知道這到底是為什麼,然後這也促使了我去看原始碼的決心。最終還是看下來了。