SSM框架開發web項目系列(四) MyBatis之快速掌握動態SQL


  前言

        通過前面的MyBatis部分學習,已經可以使用MyBatis獨立構建一個數據庫程序,基本的增刪查改/關聯查詢等等都可以實現了。簡單的單表操作和關聯查詢在實際開的業務流程中一定會有,但是可能只會占一部分,很多業務需求往往夾雜着一些需要我們在后台去判斷的參數,舉個例子,我們基本都上過購物網站,想要查看心儀的商品列表,可以通過商品分類篩選,也可以通過商品價格來篩選,還可以同時根據分類和價格來篩選,這里我們可以簡單的理解為通過商品分類和商品價格的篩選分別為select語句中where后面的2個子句,類似category=XXX和price > xxx and price <xxx,具體怎么篩選要看用戶怎么操作。如果按照之前的路子,我們要分別定義三個select方法和sql語句,這個就涉及到了一個靜態動態的問題,用戶的操作、輸入等等都是不確定的,即動態的,但是我們之前在sql映射文件中寫的語句都是針對單個操作單個想法去寫死的,即靜態的。這樣一來,隨着需求和判斷的的不斷疊加,這個代碼量會很可怕。另外一個問題,如果大家有使用Java代碼拼接過復雜SQL語句經歷,應該不會感到很方便,本人使用hibernate的時候也拼接過HQL,共同點就是那些分隔符、空格之類的寫起來很麻煩,也容易出錯。MyBatis提供了動態SQL這一特性,能同時改良上述兩種開發場景。

  總覽

        MyBatis提供的動態SQL元素實際就是通過在我們的sql語句中嵌入標簽實現動態,具體標簽如下圖所示。

       

        熟悉jsp、jstl、el表達式那套的,應該對里面大部分標簽名都不陌生,也比較容易理解,具體用法下面分別進行解析。為了結合具體的使用場景,將上面元素細分為四組來演示,分別為【if、where、trim】、【if、set、trim】、【choose、when、otherwise】、【foreach】

  背景

private Integer id;         //主鍵
private String name;        //姓名
private String gender;      //性別
private Integer age;        //年齡
private String ifIT;        //是否從事IT行業

  if、where、trim篇

  1.查詢語句中的if

  以上為我們定義的一個人的屬性,數據庫中也有一個人的數據表。現在假設需要查詢人中的所有男性,同時如果輸入參數中年齡不為空,就根據性別和年齡查詢。在沒有使用動態SQL之前,按照我們的慣有思路,我們需要在Mapper接口中定義兩個查詢方法,同時分別對應在SQL映射文件中定義兩個<select>語句,如下:

<select id="selectPerson1" parameterType="psn" resultMap="personResultMap">
    select * from person where GENDER  = '男'
</select>

<select id="selectPerson2" parameterType="psn" resultMap="personResultMap">
    select * from person where GENDER  = '男' and AGE = #{age}
</select>

  這樣一來,隨着類似的需要越來越多,我們的方法和SQL語句量會增加到很多,並且會發現,其實語句中存在很多重復部分。那么有沒有辦法能同時應對類似的相關需求,同時減少代碼量呢?動態SQL就提供了相關的功能實現這些需求,例如上述場景,我們即可只需定義一個方法,對應的SQL語句寫成如下:

<select id="selectPerson" parameterType="psn" resultMap="personResultMap">
    select * from person where GENDER  = '男'
        <if test="age != null">
            and AGE = #{age}
        </if>
</select>

  在這項<select>我們將確定的(靜態的的部分)select * from person where GENDER = '男'和后面的<if>部分結合起來,通過動態SQL提供的<if>標簽給語句預加一層判斷,test屬性值為布爾類型,true或者false,當為true(即真)時,才會把<if>標簽下的內容添加到語句中響應為值,這里的test中即判斷輸入參數中年齡是否為空,不為空則添加【and AGE = #{age}到【select * from person where GENDER = '男'】后面,為空則不加,這樣就達到了同時滿足兩種需要,但只定義了一個方法和一條SQL。

  2.查詢語句中if的where改進

  進一步擴展如果想把where后面的部分都動態化,這里以性別為例,查詢時如果參數中有不為空的性別值,則根據性別查詢,反之則查詢所有,有了前面if的學習,我們不難寫出如下動態SQL:

<select id="selectPerson1" parameterType="psn" resultMap="personResultMap">
    select * from person where
        <if test="gender != null">
            gender = #{gender}
        </if>
</select>

 

  這時候問題來了,當性別不為空時,語句是 select * from person where gender = #{gender} ,這樣還能正常查詢出我們想要的結果,但是如果性別為空,會發現語句變成了 select * from person where ,這顯然是生成一個錯誤的SQL了,為了解決類似的問題,動態SQL<where>能幫我們解決這個問題,我們可以將上述語句優化成如下:

<select id="selectPerson1" parameterType="psn" resultMap="personResultMap">
    select * from person
    <where>
        <if test="gender != null">
            gender = #{gender}
        </if>
    </where>
</select>

  這樣mybatis在這里會根據<where>標簽中是否有內容來確定要不要加上where,在這里使用<where>后,如果年齡為空,則前面引發錯誤的where也不會出現了。

  3.針對where的trim同等轉換

  進一步擴展,如果我們查詢有多個參數需要判斷,根據性別和年齡參數,有了前面<if>和<where>的了解,我們就可以寫出如下SQL:

<select id="selectPerson2" parameterType="psn" resultMap="personResultMap">
    select * from person
    <where>
        <if test="gender != null">
            gender = #{gender}
        </if>
        <if test="age != null">
            and age = #{age}
        </if>
    <where>
</select>

  乍一看基本沒什么毛病了,在這里【性別空、年齡空】、【性別不空、年齡不空】、【性別不空、年齡空】都沒問題,但是如果是【性別空、年齡不為空】,按理來說語句變成這樣 select * from person where and age = #{age} ,然后如果你動手嘗試一下,就會發現,並不會,這也體現了<where>一個強大之處,它不僅會根據元素中內容是否為空決定要不要添加where,還會自動過濾掉內容頭部的and或者or,另外空格之類的問題也會智能處理。

  MyBatis還提供了一種更靈活的<trim>標簽,在這里可以替代<where>,如上面的定義可以修改成如下,同樣可以實現效果:

<select id="selectPerson2" parameterType="psn" resultMap="personResultMap">
    select * from person
    <trim prefix="where" prefixOverrides="and |or " >
        <if test="gender != null">
            and gender = #{gender}
        </if>
        <if test="age != null">
            and age = #{age}
        </if>
    </trim>
</select>

  <trim>標簽共有四個屬性,分別為【prefix】、【prefixOverrides】、【suffix】、【suffixOverrides】,prefix代表會給<trim>標簽中內容加上的前綴,當然前提是內容不為空,prefixOverrides代表前綴過濾,過濾的是內容的前綴,suffix和suffixOverrides則分別對應后綴。例如上面的語句中如果性別和年齡都不為空,<trim>會在添加where前綴的同時,把第一個<if>中的and去掉,這樣來實現<where>同樣的功能。

  if、set、trim篇

  1.更新語句中的if

  前面都是查詢,我們換個更新試試,這里更新我們只想更新部分字段,而且要根據參數是否為空來確定是否更新,這里以姓名和性別為例。

<update id="updatePerson">
    update person set
    <if test="name != null">
        NAME = #{name},
    </if>
    <if test="gender != null">
        GENDER = #{gender}
    </if>
</update>

  2.更新語句中if的set改進

  這里如果【姓別為空】或者【性別和性別都為空】,類似之前的where問題同樣也來了,所以MyBatis同樣也提供了<set>標簽來解決這一問題,所以上面的定義可以優化成如下所示

<update id="updatePerson1">
    update person
    <set>
     
<if test="name != null"> NAME = #{name}, </if> <if test="gender != null"> GENDER = #{gender}, </if> </set>
</update>

   在這里,<set>會根據標簽中內容的有無來確定要不要加上set,同時能自動過來內容后綴逗號,但是有一點要注意,不同於<where>,當<where>中內容為空時,我們可以查出所有人的信息,但是這里更新語句中,<set>內容為空時,語句變成 update person ,還是會出錯,同時更新操作也不會生效了。

  3.針對set的trim同等轉換

  之前介紹到的<trim>靈活之處,在這里通過修改屬性值,也能用來替代<set>,上面的定義使用<trim>改寫如下所示:

<update id="updatePerson2">
    update person
    <trim prefix="set" suffixOverrides=",">
        <if test="name != null">
            NAME = #{name},
        </if>
        <if test="gender != null">
            GENDER = #{gender}
        </if>
    </trim>
</update>

  這里設置前綴為set,后綴過濾為逗號“,”,這樣一來,在if判斷之后會引發報錯的逗號將不會存在,同樣可以實現相關功能。

  choose、when、otherwise篇

  前面我們了解到的,<select>查詢語句中where后面,<if>通過判斷各個參數是否為空來確定是否添加其子句內容到where后面,是一個累加的關系,但是如果我們的需求,不是累加,而是多選一,例如姓名(name)、性別(gender)、是否從事It行業(ifIT),具體來講就是,如果優先判斷姓名是否有值,有的話就根據姓名查,沒有的話其次再判斷性別是否有值,有的話就根據性別查,也沒有的話就根據是否從事IT行業來查。這樣一來,按照前面了解到的<if><where>似乎有些頭大,不僅有多重判斷,還涉及到一個優先級先后問題,一下子似乎很難快速想到一個簡單方便的路子。MyBatis同樣提供了一套<choose>、<when>、<otherwise>來幫我們解決類似上面的問題。

  如果覺得不太好記憶,可以聯想Java中條件判斷的switch,switch對應這里的<choose>,case對應<when>,一旦某個case條件滿足了會break跳出,同時如果都不滿足,最后還有個default可以對應這里的<otherwise>,所以最終<when>和<otherwise>中有且只有一個會滿足,也就只有一項內容會被添加進去。按照上面的需求,我們可以寫出如下動態SQL:

<select id="selectPersonExt" parameterType="psn" resultMap="personResultMap">
    select * from person
    <where>
        <choose>
            <when test="name!=null">
                NAME = #{name}
            </when>
            <when test="gender!=null">
                GENDER = #{gender}
            </when>
            <otherwise>
                IF_IT = #{ifIT}
            </otherwise>
        </choose>
    </where>
</select>

  即可方便的實現該場景。

  foreach篇

  for循環大家應該都不陌生,這里的foreach同樣主要用來迭代集合,那么SQL中哪些地方會用到集合呢,用過in的應該比較熟悉,例如下面select語句:

select * from person where age in(10,20,30,40)

  上面語句可以查詢出年齡為10或20或30或40的人,這些年齡數據是一個集合,但是通過參數傳入的集合是動態的,我們不可能預知數值和像這樣寫死,MyBatis提供的<foreach>即可實現該功能。該集合作為參數傳入,以上場景方法定義和SQL語句可以寫成如下所示:

List<Person> selectForeachAge(List<Integer> ageList);
<select id="selectForeachAge" resultMap="personResultMap">
    select * from person where age in
    <foreach collection="list" item="age" index="i" open="(" close=")" separator=",">
        #{age}
    </foreach>
</select>

   這里我們可以看到,<foreach>共有6個屬性。

  item:表示集合迭代時元素的別名,這里對應#{age}中的age

  index:集合迭代時索引,用於表示集合此時迭代到的位置

  open、close、separator:這三個分別代表起始、結束、分隔符,在這里,我們用過in語句查詢時應該都知道,使用到集合的查詢語句結構關鍵部分可以描述如下所示: SELECT column_name(s) FROM table_name WHERE column_name IN (value1,value2,...) 。可以看到在IN關鍵字的后面,集合內容兩邊分別是左括號、右括號,中間集合的各個元素之間用逗號隔開,正好與這里的三個屬性分別對應。

  collection:這個屬性是必要的,在這里我們傳入的是單個參數即一個List,所以屬性值就是list。

  完整示例

  上面就是動態SQL的各個元素的基本內容,熟悉之后會讓我們編寫SQL時更加方便和靈活,下面給出完整代碼示例供參考。

  maven工程結構如下圖

  

  MyBatis配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 這里可以定義類的別名,在mapper.xml文件中應用會方便很多 -->
    <typeAliases>
        <typeAlias alias="psn" type="com.mmm.pojo.Person" />
    </typeAliases>
    <!-- 環境配置 -->
    <environments default="envir">
        <environment id="envir">
            <transactionManager type="JDBC"></transactionManager>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://192.168.0.100:3306/ssm?characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="abc123"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="com/mmm/mapper/personMapper.xml"/>
    </mappers>
</configuration>

  實體類(Person)

package com.mmm.pojo;

public class Person {
    private Integer id;            //主鍵
    private String name;        //姓名
    private String gender;        //性別
    private Integer age;        //年齡
    private String ifIT;        //是否從事IT行業
    
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
    public String getIfIT() {
        return ifIT;
    }
    public void setIfIT(String ifIT) {
        this.ifIT = ifIT;
    }
    @Override
    public String toString() {
        return "Person [id=" + id + ", name=" + name + ", gender=" + gender
                + ", age=" + age + ", ifIT=" + ifIT + "]";
    }
    
}

  Mapper接口(PersonMapper)

package com.mmm.mapper;

import java.util.List;

import com.mmm.pojo.Person;

public interface PersonMapper {
    
    //查找所有Person對象,返回集合類型,用於在測試類中查看動態SQL結果
    List<Person> selectAll();
    
    //用於測試查詢語句中if、where、trim
    List<Person> selectPerson(Person p);
    List<Person> selectPerson1(Person p);
    List<Person> selectPerson2(Person p);
    
    //用於測試更新語句中if、set、trim
    void updatePerson(Person p);
    void updatePerson1(Person p);
    void updatePerson2(Person p);
    
    //用於測試choose、where、otherwise
    List<Person> selectPersonExt(Person p);
    
    //用於測試foreach
    List<Person> selectForeachAge(List<Integer> ageList);
    
}

  SQL映射文件(personMapper.xml)

<?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="com.mmm.mapper.PersonMapper">
    <resultMap type="psn" id="personResultMap">
        <id column="ID" property="id" />
        <result column="NAME" property="name" />
        <result column="GENDER" property="gender" />
        <result column="AGE" property="age" />
        <result column="IF_IT" property="ifIT" />
    </resultMap>
    
    <select id="selectAll" resultMap="personResultMap">
        select * from person
    </select>
    
    
    <!-- 針對查詢語句中if -->
    <select id="selectPerson" parameterType="psn" resultMap="personResultMap">
        select * from person where GENDER  = '男'
            <if test="age != null">
                and AGE = #{age}
            </if>
    </select>
    
    <!-- 針對where -->
    <select id="selectPerson1" parameterType="psn" resultMap="personResultMap">
        select * from person
        <where>
            <if test="gender != null">
                gender = #{gender}
            </if>
        </where>
    </select>
    
    <!-- 針對where的 trim轉換 -->
    <select id="selectPerson2" parameterType="psn" resultMap="personResultMap">
        select * from person
        <trim prefix="where" prefixOverrides="and |or " >
            <if test="gender != null">
                and gender = #{gender}
            </if>
            <if test="age != null">
                and age = #{age}
            </if>
        </trim>
        <trim prefix="" prefixOverrides="" suffix="" suffixOverrides=""></trim>
    </select>
    
    
    <!-- 針對更新語句中if -->
    <update id="updatePerson">
        update person set
        <if test="name != null">
            NAME = #{name},
        </if>
        <if test="gender != null">
            GENDER = #{gender}
        </if>
        where ID = #{id}
    </update>
    
    <!-- 針對set -->
    <update id="updatePerson1">
        update person
        <set>
            <if test="name != null">
                NAME = #{name},
            </if>
            <if test="gender != null">
                GENDER = #{gender}
            </if>
        </set>
    </update>
    
    <!-- 針對set的trim轉換 -->
    <update id="updatePerson2">
        update person
        <trim prefix="set" suffixOverrides=",">
            <if test="name != null">
                NAME = #{name},
            </if>
            <if test="gender != null">
                GENDER = #{gender}
            </if>
        </trim>
    </update>
    
    <!-- choose when otherwise -->
    <select id="selectPersonExt" parameterType="psn" resultMap="personResultMap">
        select * from person
        <where>
            <choose>
                <when test="name!=null">
                    NAME = #{name}
                </when>
                <when test="gender!=null">
                    GENDER = #{gender}
                </when>
                <otherwise>
                    IF_IT = #{ifIT}
                </otherwise>
            </choose>
        </where>
    </select>
    
    <!-- foreach -->
    <select id="selectForeachAge" resultMap="personResultMap">
        select * from person where age in
        <foreach collection="list" item="age" index="i" open="(" close=")" separator=",">
            #{age}
        </foreach>
    </select>
    
</mapper>

  最后測試

package com.mmm.test;

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;

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.junit.Test;

import com.mmm.mapper.PersonMapper;
import com.mmm.pojo.Person;

public class TestDB {
    
    static PersonMapper mapper;
    
    static {
        //直接實例SqlSessionFactoryBuilder對象
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        //MyBatis配置文件路徑
        String path = "mybatis-config.xml";
        //通過路徑獲取輸入流
        Reader reader = null;
        try {
            reader = Resources.getResourceAsReader(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //通過reader構建sessionFactory
        SqlSessionFactory sessionFactory = builder.build(reader);
        //獲取SqlSession對象
        SqlSession sqlSession = sessionFactory.openSession();
        //獲取Mapper實例
        mapper = sqlSession.getMapper(PersonMapper.class);
    }
    
    @Test
    public void testSelect() throws Exception {
        Person p = new Person();
        //p.setAge(11);
        //p.setGender("男");
        List<Person> list = mapper.selectPerson2(p);
        for(Person psn:list) {
            System.out.println(psn);
        }
    }
    
    @Test
    public void testUpdate() throws Exception {
        Person p = new Person();
        p.setId(10001);
        //p.setName("小改2");
        //p.setGender("男");
        mapper.updatePerson2(p);
        List<Person> list = mapper.selectAll();
        for(Person psn:list) {
            System.out.println(psn);
        }

    }
    
    @Test
    public void testSelectExt() throws Exception {
        Person p1 = new Person();
        //p1.setName("小紅");
        //p.setGender("男");
        p1.setIfIT("是");
        List<Person> list = mapper.selectPersonExt(p1);
        for(Person psn:list) {
            System.out.println(psn);
        }

    }
    
    @Test
    public void testSelectForeachAge() throws Exception {
        List<Integer> ageList = new ArrayList<Integer>();
        ageList.add(21);
        ageList.add(25);
        ageList.add(36);
        List<Person> list = mapper.selectForeachAge(ageList);
        for(Person psn:list) {
            System.out.println(psn);
        }

    }
}

  小結 

  以上即為MyBatis動態SQL的內容,在測試類中可以各種嘗試改變各種輸入值,來查看效果,文中雖然每個元素都涉及到了,但是有些地方還存在不足,並未過多深入擴展,例如最后的foreach,我們的參數不一定是單個,而且也不一定是集合,這些情況我們都該怎么處理,按自己的需要再去深入學習和了解,往往很快會有深刻印象。一個問題與方法的先后問題,當遇到問題后,順着問題去找方法之后,往往很好記住。反之,沒有問題和應用場景,單純的學習方法和理論效果應該會遜色一些。最后首要還是把這些基礎的東西搞懂,再去慢慢延伸。


免責聲明!

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



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