MyBatis動態SQL(使用)整理
MyBatis 令人喜歡的一大特性就是動態 SQL。在使用 JDBC 的過程中, 根據條件進行 SQL 的拼接是很麻煩且很容易出錯的。MyBatis 動態 SQL 的出現, 解決了這個麻煩。
MyBatis通過 OGNL 來進行動態 SQL 的使用的。
目前, 動態 SQL 支援以下幾種標籤
元素 | 作用 | 備註 |
---|---|---|
if | 判斷語句 | 單條件分支 |
choose(when、otherwise) | 相當於 Java 中的 if else | 多條件分支 |
trim(where、set) | 輔助元素 | 用於處理 SQL 拼接問題 |
foreach | 迴圈語句 | 批量插入, 更新, 查詢時經常用到 |
bind | 建立一個變數, 並繫結到上下文中 | 用於相容不同的資料庫, 防止 SQL 注入等 |
1 資料準備
為了後面的演示, 建立了一個 Maven 專案 mybatis-dynamic, 建立了對應的資料庫和表
DROPTABLEIFEXISTS`student`;
CREATETABLE`student`(
`student_id`int(10)unsignedNOTNULLAUTO_INCREMENTCOMMENT'編號',
`name`varchar(20)DEFAULTNULLCOMMENT'姓名',
`phone`varchar(20)DEFAULTNULLCOMMENT'電話',
`email`varchar(50)DEFAULTNULLCOMMENT'郵箱',
`sex`tinyint(4)DEFAULTNULLCOMMENT'性別',
`locked`tinyint(4)DEFAULTNULLCOMMENT'狀態(0:正常,1:鎖定)',
`gmt_created`datetimeDEFAULTCURRENT_TIMESTAMPCOMMENT'存入資料庫的時間',
`gmt_modified`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'修改的時間',
`delete`int(11)DEFAULTNULL,
PRIMARYKEY(`student_id`)
)ENGINE=InnoDBAUTO_INCREMENT=7DEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_0900_ai_ciCOMMENT='學生表';
對應的專案結構:
2 if 標籤
if 標籤是我們最常使用的。在查詢、刪除、更新的時候很可能會使用到。必須結合 test 屬性聯合使用。
2.1 在 WHERE 條件中使用 if 標籤
這是常見的一種現象, 我們在進行按條件查詢的時候, 可能會有多種情況。
2.1.1 查詢條件
根據輸入的學生資訊進行條件檢索
- 當只輸入使用者名稱時, 使用使用者名稱進行模糊檢索;
- 當只輸入性別時, 使用性別進行完全匹配
- 當用戶名和性別都存在時, 用這兩個條件進行查詢匹配查詢
2.1.2 動態 SQL
介面函式
/**
*根據輸入的學生資訊進行條件檢索
* 1. 當只輸入使用者名稱時,使用使用者名稱進行模糊檢索;
*2.當只輸入郵箱時,使用性別進行完全匹配
*3.當用戶名和性別都存在時,用這兩個條件進行查詢匹配的用
*@paramstudent
*@return
*/
List<Student>selectByStudentSelective(Studentstudent);
對應的動態 SQL
<selectid="selectByStudentSelective"resultMap="BaseResultMap"parameterType="com.homejim.mybatis.entity.Student">
select
<includerefid="Base_Column_List"/>
fromstudent
where1=1
<iftest="name!=nullandname!=''">
andnamelikeconcat('%',#{name},'%')
</if>
<iftest="sex!=null">
andsex=#{sex}
</if>
</select>
在此 SQL 語句中, where 1=1 是多條件拼接時的小技巧, 後面的條件查詢就可以都用 and 了。
同時, 我們添加了 if 標籤來處理動態 SQL
<iftest="name!=nullandname!=''">
andnamelikeconcat('%',#{name},'%')
</if>
<iftest="sex!=null">
andsex=#{sex}
</if>
此 if 標籤的 test 屬性值是一個符合 OGNL 的表示式, 表示式可以是 true 或 false。如果表示式返回的是數值, 則0為 false, 非 0 為 true;
2.1.3 測試
@Test
publicvoidselectByStudent(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
Studentsearch=newStudent();
search.setName("明");
System.out.println("只有名字時的查詢");
List<Student>studentsByName=studentMapper.selectByStudentSelective(search);
for(inti=0;i<studentsByName.size();i++){
System.out.println(ToStringBuilder.reflectionToString(studentsByName.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
search.setName(null);
search.setSex((byte)1);
System.out.println("只有性別時的查詢");
List<Student>studentsBySex=studentMapper.selectByStudentSelective(search);
for(inti=0;i<studentsBySex.size();i++){
System.out.println(ToStringBuilder.reflectionToString(studentsBySex.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
System.out.println("姓名和性別同時存在的查詢");
search.setName("明");
List<Student>studentsByNameAndSex=studentMapper.selectByStudentSelective(search);
for(inti=0;i<studentsByNameAndSex.size();i++){
System.out.println(ToStringBuilder.reflectionToString(studentsByNameAndSex.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
sqlSession.commit();
sqlSession.close();
}
只有名字時的查詢, 傳送的語句和結果
只有名字時的查詢
查詢的條件只發送了
where1=1andnamelikeconcat('%',?,'%')
只有性別時的查詢, 傳送的語句和結果
只有性別時的查詢
查詢的條件只發送了
where1=1andsex=?
姓名和性別同時存在的查詢, 傳送的語句和結果
姓名和性別同時存在的查詢
查詢條件
where1=1andnamelikeconcat('%',?,'%')andsex=?
2.2 在 UPDATE 更新列中使用 if 標籤
有時候我們不希望更新所有的欄位, 只更新有變化的欄位。
2.2.1 更新條件
只更新有變化的欄位, 空值不更新。
2.2.1 動態 SQL
介面方法
/**
*更新非空屬性
*/
intupdateByPrimaryKeySelective(Studentrecord);
對應的 SQL
<updateid="updateByPrimaryKeySelective"parameterType="com.homejim.mybatis.entity.Student">
updatestudent
<set>
<iftest="name!=null">
`name`=#{name,jdbcType=VARCHAR},
</if>
<iftest="phone!=null">
phone=#{phone,jdbcType=VARCHAR},
</if>
<iftest="email!=null">
email=#{email,jdbcType=VARCHAR},
</if>
<iftest="sex!=null">
sex=#{sex,jdbcType=TINYINT},
</if>
<iftest="locked!=null">
locked=#{locked,jdbcType=TINYINT},
</if>
<iftest="gmtCreated!=null">
gmt_created=#{gmtCreated,jdbcType=TIMESTAMP},
</if>
<iftest="gmtModified!=null">
gmt_modified=#{gmtModified,jdbcType=TIMESTAMP},
</if>
</set>
wherestudent_id=#{studentId,jdbcType=INTEGER}
2.2.3 測試
@Test
publicvoidupdateByStudentSelective(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
Studentstudent=newStudent();
student.setStudentId(1);
student.setName("明明");
student.setPhone("13838438888");
System.out.println(studentMapper.updateByPrimaryKeySelective(student));
sqlSession.commit();
sqlSession.close();
}
結果如下
在 UPDATE 更新列中使用 if 標籤
2.3 在 INSERT 動態插入中使用 if 標籤
我們插入資料庫中的一條記錄, 不是每一個欄位都有值的, 而是動態變化的。在這時候使用 if 標籤, 可幫我們解決這個問題。
2.3.1 插入條件
只有非空屬性才插入。
2.3.2 動態SQL
介面方法
/**
*非空欄位才進行插入
*/
intinsertSelective(Studentrecord);
對應的SQL
<insertid="insertSelective"parameterType="com.homejim.mybatis.entity.Student">
insertintostudent
<trimprefix="("suffix=")"suffixOverrides=",">
<iftest="studentId!=null">
student_id,
</if>
<iftest="name!=null">
`name`,
</if>
<iftest="phone!=null">
phone,
</if>
<iftest="email!=null">
email,
</if>
<iftest="sex!=null">
sex,
</if>
<iftest="locked!=null">
locked,
</if>
<iftest="gmtCreated!=null">
gmt_created,
</if>
<iftest="gmtModified!=null">
gmt_modified,
</if>
</trim>
<trimprefix="values("suffix=")"suffixOverrides=",">
<iftest="studentId!=null">
#{studentId,jdbcType=INTEGER},
</if>
<iftest="name!=null">
#{name,jdbcType=VARCHAR},
</if>
<iftest="phone!=null">
#{phone,jdbcType=VARCHAR},
</if>
<iftest="email!=null">
#{email,jdbcType=VARCHAR},
</if>
<iftest="sex!=null">
#{sex,jdbcType=TINYINT},
</if>
<iftest="locked!=null">
#{locked,jdbcType=TINYINT},
</if>
<iftest="gmtCreated!=null">
#{gmtCreated,jdbcType=TIMESTAMP},
</if>
<iftest="gmtModified!=null">
#{gmtModified,jdbcType=TIMESTAMP},
</if>
</trim>
</insert>
這個 SQL 大家應該很熟悉, 畢竟是自動生成的。
2.3.3 測試
@Test
publicvoidinsertByStudentSelective(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
Studentstudent=newStudent();
student.setName("小飛機");
student.setPhone("13838438899");
student.setEmail("[email protected]");
student.setLocked((byte)0);
System.out.println(studentMapper.insertSelective(student));
sqlSession.commit();
sqlSession.close();
}
對應的結果
在 INSERT 動態插入中使用 if 標籤
SQL 中, 只有非空的欄位才進行了插入。
3 choose 標籤
choose when otherwise 標籤可以幫我們實現 if else 的邏輯。
一個 choose 標籤至少有一個 when,最多一個otherwise
下面是一個查詢的例子。
3.1 查詢條件
假設 name 具有唯一性, 查詢一個學生
- 當 studen_id 有值時, 使用 studen_id 進行查詢;
- 當 studen_id 沒有值時, 使用 name 進行查詢;
- 否則返回空
3.2 動態SQL
介面方法
/**
*-當 studen_id 有值時,使用 studen_id 進行查詢;
*-當 studen_id 沒有值時,使用 name 進行查詢;
*-否則返回空
*/
StudentselectByIdOrName(Studentrecord);
對應的SQL
<selectid="selectByIdOrName"resultMap="BaseResultMap"parameterType="com.homejim.mybatis.entity.Student">
select
<includerefid="Base_Column_List"/>
fromstudent
where1=1
<choose>
<whentest="studentId!=null">
andstudent_id=#{studentId}
</when>
<whentest="name!=nullandname!=''">
andname=#{name}
</when>
<otherwise>
and1=2
</otherwise>
</choose>
</select>
3.3 測試
@Test
publicvoidselectByIdOrName(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
Studentstudent=newStudent();
student.setName("小飛機");
student.setStudentId(1);
StudentstudentById=studentMapper.selectByIdOrName(student);
System.out.println("有ID則根據ID獲取");
System.out.println(ToStringBuilder.reflectionToString(studentById,ToStringStyle.MULTI_LINE_STYLE));
student.setStudentId(null);
StudentstudentByName=studentMapper.selectByIdOrName(student);
System.out.println("沒有ID則根據name獲取");
System.out.println(ToStringBuilder.reflectionToString(studentByName,ToStringStyle.MULTI_LINE_STYLE));
student.setName(null);
StudentstudentNull=studentMapper.selectByIdOrName(student);
System.out.println("沒有ID和name,返回null");
Assert.assertNull(studentNull);
sqlSession.commit();
sqlSession.close();
}
有 ID 則根據 ID 獲取, 結果
有 ID 則根據 ID 獲取
沒有 ID 則根據 name 獲取
沒有 ID 則根據 name 獲取
沒有 ID 和 name, 返回 null
沒有 ID 和 name, 返回 null
4 trim(set、where)
這三個其實解決的是類似的問題。如我們在寫前面的[在 WHERE 條件中使用 if 標籤] SQL 的時候, where 1=1 這個條件我們是不希望存在的。
4.1 where
4.1.1 查詢條件
根據輸入的學生資訊進行條件檢索。
- 當只輸入使用者名稱時, 使用使用者名稱進行模糊檢索;
- 當只輸入性別時, 使用性別進行完全匹配
- 當用戶名和性別都存在時, 用這兩個條件進行查詢匹配查詢
不使用 where 1=1。
4.1.2 動態 SQL
很顯然, 我們要解決這幾個問題
- 當條件都不滿足時:此時 SQL 中應該要不能有 where , 否則導致出錯
- 當 if 有條件滿足時:SQL 中需要有 where, 且第一個成立的 if 標籤下的 and | or 等要去掉
這時候, 我們可以使用 where 標籤。
介面方法
/**
*根據輸入的學生資訊進行條件檢索
* 1. 當只輸入使用者名稱時,使用使用者名稱進行模糊檢索;
*2.當只輸入郵箱時,使用性別進行完全匹配
*3.當用戶名和性別都存在時,用這兩個條件進行查詢匹配的用
*/
List<Student>selectByStudentSelectiveWhereTag(Studentstudent);
對應的 SQL
<selectid="selectByStudentSelectiveWhereTag"resultMap="BaseResultMap"parameterType="com.homejim.mybatis.entity.Student">
select
<includerefid="Base_Column_List"/>
fromstudent
<where>
<iftest="name!=nullandname!=''">
andnamelikeconcat('%',#{name},'%')
</if>
<iftest="sex!=null">
andsex=#{sex}
</if>
</where>
</select>
4.1.3 測試
@Test
publicvoidselectByStudentWhereTag(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
Studentsearch=newStudent();
search.setName("明");
System.out.println("只有名字時的查詢");
List<Student>studentsByName=studentMapper.selectByStudentSelectiveWhereTag(search);
for(inti=0;i<studentsByName.size();i++){
System.out.println(ToStringBuilder.reflectionToString(studentsByName.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
search.setSex((byte)1);
System.out.println("姓名和性別同時存在的查詢");
List<Student>studentsBySex=studentMapper.selectByStudentSelectiveWhereTag(search);
for(inti=0;i<studentsBySex.size();i++){
System.out.println(ToStringBuilder.reflectionToString(studentsBySex.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
System.out.println("姓名和性別都不存在時查詢");
search.setName(null);
search.setSex(null);
List<Student>studentsByNameAndSex=studentMapper.selectByStudentSelectiveWhereTag(search);
for(inti=0;i<studentsByNameAndSex.size();i++){
System.out.println(ToStringBuilder.reflectionToString(studentsByNameAndSex.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
sqlSession.commit();
sqlSession.close();
}
只有名字時的查詢, 有 where
只有名字時的查詢
姓名和性別同時存在的查詢, 有 where
姓名和性別同時存在的查詢
姓名和性別都不存在時查詢, 此時, where 不會再出現了。
姓名和性別都不存在時查詢
4.2 set
set 標籤也類似, 在 [2.2 在 UPDATE 更新列中使用 if 標籤] 中, 如果我們的方法updateByPrimaryKeySelective
沒有使用 標籤, 那麼我們就要想辦法處理欄位全為空的條件, 欄位不為空的條件等。有了這個, 我們只需要寫 if 標籤即可, 不需要處理類似的問題。
4.3 trim
set 和 where 其實都是 trim 標籤的一種型別, 該兩種功能都可以使用 trim 標籤進行實現。
4.3.1 trim 來表示 where
如以上的 where 標籤, 我們也可以寫成
<trimprefix="where"prefixOverrides="AND|OR">
</trim>
表示當 trim 中含有內容時, 新增 where, 且第一個為 and 或 or 時, 會將其去掉。而如果沒有內容, 則不新增 where。
4.3.2 trim 來表示 set
相應的, set 標籤可以如下表示
<trimprefix="SET"suffixOverrides=",">
</trim>
表示當 trim 中含有內容時, 新增 set, 且最後的內容為 , 時, 會將其去掉。而沒有內容, 不新增 set
4.3.3 trim 的幾個屬性
- prefix: 當 trim 元素包含有內容時, 增加 prefix 所指定的字首
- prefixOverrides: 當 trim 元素包含有內容時, 去除 prefixOverrides 指定的 字首
- suffix: 當 trim 元素包含有內容時, 增加 suffix 所指定的字尾
- suffixOverrides:當 trim 元素包含有內容時, 去除 suffixOverrides 指定的字尾
5 foreach 標籤
foreach 標籤可以對陣列, Map 或實現 Iterable 介面。
foreach 中有以下幾個屬性
- collection: 必填, 集合/陣列/Map的名稱.
- item: 變數名。即從迭代的物件中取出的每一個值
- index: 索引的屬性名。當迭代的物件為 Map 時, 該值為 Map 中的 Key.
- open: 迴圈開頭的字串
- close: 迴圈結束的字串
- separator: 每次迴圈的分隔符
其他的比較好理解, collection 中的值應該怎麼設定呢?
跟介面方法中的引數相關。
1. 只有一個數組引數或集合引數
預設情況:集合collection=list, 陣列是collection=array
推薦:使用 @Param 來指定引數的名稱, 如我們在引數前@Param("ids"), 則就填寫 collection=ids
2. 多引數
多引數請使用 @Param 來指定, 否則SQL中會很不方便
3. 引數是Map
指定為 Map 中的對應的 Key 即可。其實上面的 @Param 最後也是轉化為 Map 的。
4. 引數是物件
使用屬性.屬性即可。
5.1 在 where 中使用 foreach
在 where條件中使用, 如按id集合查詢, 按id集合刪除等。
5.1.1 查詢條件
我們希望查詢使用者 id 集合中的所有使用者資訊。
5.1.2 動態 SQL
函式介面
/**
*獲取id集合中的使用者資訊
*@paramids
*@return
*/
List<Student>selectByStudentIdList(List<Integer>ids);
對應 SQL
<selectid="selectByStudentIdList"resultMap="BaseResultMap">
select
<includerefid="Base_Column_List"/>
fromstudent
wherestudent_idin
<foreachcollection="list"item="id"open="("close=")"separator=","index="i">
#{id}
</foreach>
</select>
5.1.3 測試
@Test
publicvoidselectByStudentIdList(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
List<Integer>ids=newLinkedList<>();
ids.add(1);
ids.add(3);
List<Student>students=studentMapper.selectByStudentIdList(ids);
for(inti=0;i<students.size();i++){
System.out.println(ToStringBuilder.reflectionToString(students.get(i),ToStringStyle.MULTI_LINE_STYLE));
}
sqlSession.commit();
sqlSession.close();
}
結果
在 where 中使用 foreach
5.2 foreach 實現批量插入
可以通過foreach來實現批量插入。
5.2.1 動態SQL
介面方法
/**
*批量插入學生
*/
intinsertList(List<Student>students);
對應的SQL
<insertid="insertList">
insertintostudent(name,phone,email,sex,locked)
values
<foreachcollection="list"item="student"separator=",">
(
#{student.name},#{student.phone},#{student.email},
#{student.sex},#{student.locked}
)
</foreach>
</insert>
5.2.2 測試
@Test
publicvoidinsertList(){
SqlSessionsqlSession=null;
sqlSession=sqlSessionFactory.openSession();
StudentMapperstudentMapper=sqlSession.getMapper(StudentMapper.class);
List<Student>students=newLinkedList<>();
Studentstu1=newStudent();
stu1.setName("批量01");
stu1.setPhone("13888888881");
stu1.setLocked((byte)0);
stu1.setEmail("[email protected]");
stu1.setSex((byte)1);
students.add(stu1);
Studentstu2=newStudent();
stu2.setName("批量02");
stu2.setPhone("13888888882");
stu2.setLocked((byte)0);
stu2.setEmail("[email protected]");
stu2.setSex((byte)0);
students.add(stu2);
System.out.println(studentMapper.insertList(students));
sqlSession.commit();
sqlSession.close();
}
結果
foreach 實現批量插入
6 bind 標籤
bind 標籤是通過 OGNL 表示式去定義一個上下文的變數, 這樣方便我們使用。
如在selectByStudentSelective
方法中, 有如下
<iftest="name!=nullandname!=''">
andnamelikeconcat('%',#{name},'%')
</if>
在 MySQL 中, 該函式支援多引數, 但在 Oracle 中只支援兩個引數。那麼我們可以使用 bind 來讓該 SQL 達到支援兩個資料庫的作用
<iftest="name!=nullandname!=''">
<bindname="nameLike"value="'%'+name+'%'"/>
andnamelike#{nameLike}
</if>
更改後的查詢結果如下