Mybatis動態SQL

constructor與discriminator鑑別器

通過修改對象屬性的方式,可以滿足大多數的數據傳輸對象(Data Transfer Object,DTO)以及絕大部分領域模型的要求。 但有些情況下你想使用不可變類。 通常來說,很少或基本不變的、包含引用或查詢數 據的表,很適合使用不可變類。 構造方法注入允許你在初始化時 爲類設置屬性的值,而不用暴露出公有方法。MyBatis 也支持私有屬性和私有 JavaBeans 屬 性來達到這個目的,但有一些人更青睞於構造方法注入。constructor 元素就是爲此而生的。

我們來看看Student類中的構造器:

package org.zero01.pojo;

public class Student {

    private int sid;
    private String sname;
    private int age;
    private String sex;
    private String address;

    public Student(Integer sid, String sname, Integer age, String sex, String address) {
        this.sid = sid;
        this.sname = sname;
        this.age = age;
        this.sex = sex;
        this.address = address;
    }
    ... getter 略 ...
}

注:類中可以寫或不寫getter setter方法,也可以只寫 getter 方法或 setter 方法。

爲了將結果注入構造方法,MyBatis需要通過某種方式定位相應的構造方法。 在下面的例子中,MyBatis搜索一個聲明瞭五個形參的的構造方法,以javaType屬性中指定的值來進行構造方法參數的排列順序:

<?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="org.zero01.dao.StudentMapper">
    <resultMap id="stuMap" type="Student">
        <constructor>
            <idArg column="sid" javaType="Integer"/>
            <arg  column="sname" javaType="String"/>
            <arg column="age" javaType="Integer"/>
            <arg column="sex" javaType="String"/>
            <arg column="address" javaType="String"/>
        </constructor>
    </resultMap>
    <select id="selectAll" resultMap="stuMap">
      select * from student
    </select>
</mapper>

編寫一個簡單的測試用例,測試能否正常往構造器中注入數據:

package org.zero01.test;

import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.zero01.dao.StudentMapper;
import org.zero01.pojo.Student;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class TestStudent {

    private SqlSession sqlSession;
    private StudentMapper studentMapper;

    @Before
    public void startTest() throws IOException {
        String confPath = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(confPath);
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sqlSessionFactory.openSession();
        studentMapper = sqlSession.getMapper(StudentMapper.class);
    }

    @After
    public void endTest() {
        if (sqlSession != null) {
            sqlSession.close();
        }
    }

    @Test
    public void testSelectAll() {
        List<Student> studentList = studentMapper.selectAll();
        for (Student student : studentList) {
            System.out.println(new JSONObject(student));
        }
    }
}

控制檯輸出結果:

{"address":"湖北","sname":"One","sex":"男","age":15,"sid":1}
{"address":"杭州","sname":"Jon","sex":"男","age":16,"sid":2}

當你在處理一個帶有多個形參的構造方法時,很容易在保證 arg 元素的正確順序上出錯。 從版本 3.4.3 開始,可以在指定參數名稱的前提下,以任意順序編寫 arg 元素。 爲了通過名稱來引用構造方法參數,你可以添加 @Param 註解,或者使用 '-parameters' 編譯選項並啓用 useActualParamName 選項(默認開啓)來編譯項目。 下面的例子對於同一個構造方法依然是有效的,儘管第三和第四個形參順序與構造方法中聲明的順序不匹配:

<?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="org.zero01.dao.StudentMapper">
    <resultMap id="stuMap" type="Student">
        <constructor>
            <idArg column="sid" javaType="Integer" name="sid"/>
            <arg column="sname" javaType="String" name="sname"/>
            <arg column="sex" javaType="String" name="sex"/>
            <arg column="age" javaType="Integer" name="age"/>
            <arg column="address" javaType="String" name="address"/>
        </constructor>
    </resultMap>
    <select id="selectAll" resultMap="stuMap">
      select * from student
    </select>
</mapper>

如果類中存在名稱和類型相同的屬性,那麼可以省略 javaType 。

剩餘的屬性和規則和普通的 id 和 result 元素是一樣的。

屬性 描述
column 數據庫中的列名,或者是列的別名。一般情況下,這和 傳遞給 resultSet.getString(columnName) 方法的參數一樣。
javaType 一個 Java 類的完全限定名,或一個類型別名(參考上面內建類型別名的列表)。 如果你映射到一個 JavaBean,MyBatis 通常可以斷定類型。然而,如 果你映射到的是 HashMap,那麼你應該明確地指定 javaType 來保證期望的 行爲。
jdbcType JDBC 類型,所支持的 JDBC 類型參見這個表格之前的“支持的 JDBC 類型”。 只需要在可能執行插入、更新和刪除的允許空值的列上指定 JDBC 類型。這是 JDBC 的要求而非 MyBatis 的要求。如果你直接面向 JDBC 編程,你需要對可能爲 null 的值指定這個類型。
typeHandler 我們在前面討論過的默認類型處理器。使用這個屬性,你可以覆蓋默 認的類型處理器。這個屬性值是一個類型處理 器實現類的完全限定名,或者是類型別名。
select 用於加載複雜類型屬性的映射語句的 ID,它會從 column 屬性中指定的列檢索數據,作爲參數傳遞給此 select 語句。具體請參考 Association 標籤。
resultMap ResultMap 的 ID,可以將嵌套的結果集映射到一個合適的對象樹中,功能和 select 屬性相似,它可以實現將多表連接操作的結果映射成一個單一的ResultSet。這樣的ResultSet將會將包含重複或部分數據重複的結果集正確的映射到嵌套的對象樹中。爲了實現它, MyBatis允許你 “串聯” ResultMap,以便解決嵌套結果集的問題。想了解更多內容,請參考下面的Association元素。
select 構造方法形參的名字。從3.4.3版本開始,通過指定具體的名字,你可以以任意順序寫入arg元素。參看上面的解釋。

discriminator鑑別器

有時一個單獨的數據庫查詢也許返回很多不同 (但是希望有些關聯) 數據類型的結果集。 鑑別器元素就是被設計來處理這個情況的, 還有包括類的繼承層次結構。 鑑別器非常容易理解,因爲它的表現很像 Java 語言中的 switch 語句。

我們先新增兩個Student的子類,MaleStudent類:

package org.zero01.pojo;

public class MaleStudent extends Student{
}

FemaleStudent類:

package org.zero01.pojo;

public class FemaleStudent extends Student {
}

定義鑑別器指定了 column 和 javaType 屬性。 column 是 MyBatis 查找比較值的地方。 JavaType 是需要被用來保證等價測試的合適類型(儘管字符串在很多情形下都會有用)。比如:

<?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="org.zero01.dao.StudentMapper">
    <resultMap id="stuMap" type="Student">
        <id property="sid" column="sid"/>
        <result property="sname" column="sname"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <result property="address" column="address"/>
        <!-- column:指定判斷的列名,javaType:column值對應的java類型 -->
        <discriminator javaType="String" column="sex">
            <!-- sex列的值爲 “男” 時,就把結果集包裝成MaleStudent對象 -->
            <case value="男" resultType="MaleStudent"/>
            <!-- sex列的值爲 “女” 時,就把結果集包裝成FemaleStudent對象 -->
            <case value="女" resultType="FemaleStudent"/>
        </discriminator>
    </resultMap>
    <select id="selectAll" resultMap="stuMap">
      select * from student
    </select>
</mapper>

修改測試方法如下:

@Test
public void testSelectAll() {
    List<Student> studentList = studentMapper.selectAll();
    for (Student student : studentList) {
        System.out.println(new JSONObject(student));
        System.out.println(student.getClass().getName() + "\n");
    }
}

運行成功後,控制檯輸出結果如下:

{"address":"湖北","sname":"One","sex":"男","age":15,"sid":1}
org.zero01.pojo.MaleStudent

{"address":"杭州","sname":"Jon","sex":"男","age":16,"sid":2}
org.zero01.pojo.MaleStudent

{"address":"湖南","sname":"Max","sex":"女","age":18,"sid":3}
org.zero01.pojo.FemaleStudent

{"address":"廣州","sname":"Alen","sex":"女","age":19,"sid":4}
org.zero01.pojo.FemaleStudent

從控制檯輸出結果中,可以看到,我們成功通過鑑別器,將不同的性別的結果集數據封裝到了不同的子類中。在case元素中,還可以使用resultMap屬性引用某個結果集的映射器,以及可以直接在case元素中使用result等元素進行結果集的封裝。例如:

<resultMap id="vehicleResult" type="Vehicle">
  <id property="id" column="id" />
  <result property="vin" column="vin"/>
  <result property="year" column="year"/>
  <result property="make" column="make"/>
  <result property="model" column="model"/>
  <result property="color" column="color"/>
  <discriminator javaType="int" column="vehicle_type">
    <case value="1" resultType="carResult">
      <result property="doorCount" column="door_count" />
    </case>
    <case value="2" resultType="truckResult">
      <result property="boxSize" column="box_size" />
      <result property="extendedCab" column="extended_cab" />
    </case>
    <case value="3" resultType="vanResult">
      <result property="powerSlidingDoor" column="power_sliding_door" />
    </case>
    <case value="4" resultType="suvResult">
      <result property="allWheelDrive" column="all_wheel_drive" />
    </case>
  </discriminator>
</resultMap>

Mybatis動態SQL

MyBatis 的強大特性之一便是它的動態 SQL。如果你有使用 JDBC 或其它類似框架的經驗,你就能體會到根據不同條件拼接 SQL 語句的痛苦。例如拼接時要確保不能忘記添加必要的空格,還要注意去掉列表最後一個列名的逗號。利用動態 SQL 這一特性可以徹底擺脫這種痛苦。雖然在以前使用動態 SQL 並非一件易事,但正是 MyBatis 提供了可以被用在任意 SQL 映射語句中的強大的動態 SQL 語言得以改進這種情形。

動態 SQL 元素和 JSTL 或基於類似 XML 的文本處理器相似。在 MyBatis 之前的版本中,有很多元素需要花時間瞭解。MyBatis 3 大大精簡了元素種類,現在只需學習原來一半的元素便可。MyBatis 採用功能強大的基於 OGNL 的表達式來淘汰其它大部分元素。MyBatis 3 只需要學習以下元素即可:

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach

(1) if 元素:

if 元素通常要做的事情是根據條件動態生成 where 子句的一部分。比如:

<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where sid = #{sid}
    <if test="sname != null">
      and sname = #{sname}
    </if>
</select>

這條語句提供了一種可選的查找文本功能。如果沒有傳入“sname”,那麼只會查詢sid相匹配的記錄;反之若傳入了“sname”,那麼就會增多一個“sname”字段的匹配條件(細心的讀者可能會發現,“title”參數值是可以包含一些掩碼或通配符的)。

和if語句一樣,除了可以使用!=、<=、>=等運算符外,if元素也可以使用 and、or、not之類的運算符,如下:

<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where sid = #{sid}
    <if test="sname != null and sex !=null ">
      and sname = #{sname}
    </if>
</select>

(2)choose, when, otherwise元素:

有時候我們需要使用分支條件判斷,針對這種情況,MyBatis 提供了 choose 元素,它有點像 Java 中的 switch 語句。例如:

<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where sid = #{sid}
    <choose>
        <when test="sname != null">
            and sname = #{sname}
        </when>
        <when test="sex != null">
            and sex = #{sex}
        </when>
        <when test="address != null">
            and address = #{address}
        </when>
        <otherwise>
            and age > 0
        </otherwise>
    </choose>
</select>

(2)trim, where, set元素:

前面幾個例子已經合宜地解決了一個臭名昭著的動態 SQL 問題。現在回到“if”示例,這次我們將sid = #{sid}也設置成動態的條件,看看會發生什麼:

<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    where
    <if test="sid != null">
        sid = #{sid}
    </if>
    <if test="sname != null and sex !=null ">
        and sname = #{sname}
    </if>
</select>

如果這些條件沒有一個能匹配上會發生什麼?最終這條 SQL 會變成這樣:

SELECT * FROM student
WHERE

這會導致查詢失敗。如果僅僅第二個條件匹配又會怎樣?這條 SQL 最終會是這樣:

SELECT * FROM student
WHERE and sname = #{sname}

顯而易見,這個查詢也會失敗。這個問題不能簡單地用條件句式來解決,如果你也曾經被迫這樣寫過,那麼你很可能從此以後都不會再寫出這種語句了。

好在 MyBatis 中有一個簡單的處理,這在 90% 的情況下都會有用。而在不能使用的地方,你可以自定義處理方式來令其正常工作。我們只需要把sql語句中 where 替換成 MyBatis 中的 where元素即可,如下:

<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    <where>
        <if test="sid != null">
            sid = #{sid}
        </if>
        <if test="sname != null and sex !=null ">
            and sname = #{sname}
        </if>
    </where>
</select>

where 元素只會在至少有一個子元素的條件返回 SQL 子句的情況下才去插入“WHERE”子句,如果沒有 SQL 子句的返回則不會插入“WHERE”子句。而且,若語句的開頭爲“AND”或“OR”,where 元素也會將它們去除。

如果 where 元素沒有按正常套路出牌,我們可以通過自定義 trim 元素來定製 where 元素的功能。比如,和 where 元素等價的自定義 trim 元素爲:

<select id="findStudentById" resultType="Student" parameterType="Student">
    select * from student
    <trim prefix="where" prefixOverrides="AND | OR ">
        <if test="sid != null">
            sid = #{sid}
        </if>
        <if test="sname != null and sex !=null ">
            and sname = #{sname}
        </if>
    </trim>
</select>

prefixOverrides 屬性可以忽略通過管道分隔的文本序列(注意此例中的空格也是必要的)。它的作用是移除所有指定在 prefixOverrides 屬性中的內容,並且插入 prefix 屬性中指定的內容。但是 prefixOverrides 屬性移除的是文本中前面的內容,例如有一段文本內容如下:

AND sname = #{sname} AND 

然後指定 prefixOverrides 屬性的值爲 “AND” ,由於 prefixOverrides 屬性只會移除前面的AND,所以移除後的文件內容如下:

sname = #{sname} AND 

與之相對應的屬性是 suffixOverrides ,它的作用是移除所有指定在 suffixOverrides 屬性中的內容,而它移除的是文本後面的內容。例如,在執行update更新語句的時候,我們也希望至少有一個子元素的條件返回 SQL 子句的情況下才去插入 “SET” 子句,而且,若語句的結尾爲 “ , ” 時需要將它們去除。使用trim元素實現如下:

<update id="updateStudent">
    update student
    <trim prefix="set" suffixOverrides=",">
        <if test="sname != null ">sname = #{sname},</if>
        <if test="age != 0 ">age = #{age},</if>
        <if test="sex != null ">sex = #{sex},</if>
        <if test="address != null ">address = #{address}</if>
    </trim>
    where sid = #{sid}
</update>

suffixOverrides屬性會把語句末尾中的逗號刪除,同樣的,prefix屬性會將指定的內容插入到語句的開頭。與prefix屬性相對應的是suffix屬性,該屬性會將指定的內容插入到語句的末尾。

以上我們使用trim元素實現了動態的更新語句,這種方式還有些麻煩,其實還可以更簡單,使用set元素即可,如下:

<update id="updateStudent">
    update student
    <set>
        <if test="sname != null ">sname = #{sname},</if>
        <if test="age != 0 ">age = #{age},</if>
        <if test="sex != null ">sex = #{sex},</if>
        <if test="address != null ">address = #{address}</if>
    <set>
    where sid = #{sid}
</update>

set 元素會動態前置 SET 關鍵字,同時也會刪掉無關的逗號,因爲用了條件語句之後很可能就會在生成的 SQL 語句的後面留下這些逗號。(因爲用的是“if”元素,若最後一個“if”沒有匹配上而前面的匹配上,SQL 語句的最後就會有一個逗號遺留)


(4)foreach元素:

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

<select id="selectStudentIn" resultMap="stuMap">
  select * from student where sid in
  <foreach collection="list" index="index" item="item" open="(" separator="," close=")">
      #{item}
  </foreach>
</select>

foreach 元素的功能非常強大,它允許你指定一個集合,聲明可以在元素體內使用的集合項(item)和索引(index)變量。它也允許你指定開頭與結尾的字符串以及在迭代結果之間放置分隔符。這個元素是很智能的,因此它不會偶然地附加多餘的分隔符:

  • collection屬性指定接收的是什麼集合
  • open屬性指定開頭的符號
  • close屬性指定結尾的符號
  • separator屬性指定迭代結果之間的分隔符
  • item屬性存儲每次迭代的集合元素(map集合時爲值)
  • index屬性存儲每次迭代的索引(map集合時爲鍵)

測試代碼如下:

@Test
public void testSelectPostIn() {
    List<Integer> idList = new ArrayList<Integer>();
    for (int i = 1; i <= 4; i++) {
        idList.add(i);
    }

    List<Student> studentList = studentMapper.selectStudentIn(idList);
    for (Student student : studentList) {
        System.out.println(new JSONObject(student));
        System.out.println(student.getClass().getName() + "\n");
    }
}

運行該方法後,最終生成出來的sql語句爲:

SELECT * FROM student WHERE sid IN (1,2,3,4)

注意:

你可以將任何可迭代對象(如 List、Set 等)、Map 對象或者數組對象傳遞給 foreach 作爲集合參數。當使用可迭代對象或者數組時,index 是當前迭代的次數,item 的值是本次迭代獲取的元素。當使用 Map 對象(或者 Map.Entry 對象的集合)時,index 是鍵,item 是值。


(5)bind元素:

bind 元素可以從 OGNL 表達式中創建一個變量並將其綁定到上下文。比如:

<select id="selectBlogsLike" resultType="Blog">
  <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
  SELECT * FROM BLOG
  WHERE title LIKE #{pattern}
</select>

(6)sql、include元素:

sql元素用來定義可重用的 SQL 代碼段,這些代碼段可以被包含在其他語句中,它可以被靜態地(在加載參數) 參數化。而include元素就是用來引入sql元素所定義的可重用 SQL 代碼段的,如下示例:

<sql id="base_column_list">
    sid,sname,age,sex,address
</sql>
<select id="selectAll" resultMap="stuMap">
  select
  <include refid="base_column_list"/>
  from student
</select>

最終生成出來的sql語句爲:

select sid,sname,age,sex,address from student

sql元素中也可以使用include元素,例如:

<sql id="someinclude">
  from
    <include refid="${include_target}"/>
</sql>

關於返回null值:

有時候我們的表格中的某個列可能會存儲了一些null值,如下表格:
Mybatis動態SQL

當某個列存在null值的話,我們使用數據庫的內置函數進行求和、統計之類的操作時,可能會剛好操作的記錄的同一個字段都是null,那麼返回的結果集就會是null。例如,我們對上面那張表格進行sum求和操作:

<select id="selectBySumAge" resultType="int">
    SELECT SUM(age) FROM student
</select>

如果我們在dao層接口方法中聲明的返回值是基本數據類型的話,就會報錯,如下:

...
public interface StudentMapper {
    ...
    int selectBySumAge();
}

測試用例代碼:

@Test
public void testSelectBySumAge() {
    Integer sumAge = studentMapper.selectBySumAge();
    System.out.println(sumAge);
}

這時運行測試用例的話,就會報如下錯誤:

org.apache.ibatis.binding.BindingException: Mapper method 'org.zero01.dao.StudentMapper.selectBySumAge attempted to return null from a method with a primitive return type (int).

    at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:93)
    at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
    at com.sun.proxy.$Proxy5.selectBySumAge(Unknown Source)
    at org.zero01.test.TestStudent.testSelectBySumAge(TestStudent.java:87)
    ... 略

會報這個錯誤是因爲int這種基本數據類型是無法接收null的,只能使用包裝類型進行接收。

爲了解決這個問題,我們需要把dao層接口方法的返回值修改爲Integer類型,如下:

...
public interface StudentMapper {
    ...
    Integer selectBySumAge();
}

xml中的resultType可以使用int,但是爲了防止意外問題,建議還是使用Integer包裝類型:

<select id="selectBySumAge" resultType="java.lang.Integer">
    SELECT SUM(age) FROM student
</select>

測試代碼不變,運行後,控制檯輸出內容如下:

null

除了在代碼層解決這個問題外,還可以在sql中解決這個問題,以sum求和示例,使用以下幾種sql語句,可以避免返回null值:

/* 第一種: 採用 IFNULL(expr1,expr2)函數,當expr1爲NULL時,則數據返回默認值expre2 */
SELECT IFNULL(SUM(age),0) FROM student  /* 若 SUM() 函數結果返回爲 NULL 則返回 0 */

/* 第二種: 採用從 COALESCE(value,...) 函數, COALESCE 函數作用是返回傳入參數中第一個非空的值 */
SELECT COALESCE(SUM(age),0) FROM student

/* 第三種: 採用 case WHEN THEN WHEN THEN .. ELSE END 函數,注意 CASE WHEN 函數最後是以 END 結尾 */
SELECT CASE WHEN ISNULL(SUM(age)) THEN 0 ELSE SUM(age) END AS ageSum FROM student
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章