前言
通過前面的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,我們的參數不一定是單個,而且也不一定是集合,這些情況我們都該怎么處理,按自己的需要再去深入學習和了解,往往很快會有深刻印象。一個問題與方法的先后問題,當遇到問題后,順着問題去找方法之后,往往很好記住。反之,沒有問題和應用場景,單純的學習方法和理論效果應該會遜色一些。最后首要還是把這些基礎的東西搞懂,再去慢慢延伸。