1. 程式人生 > 實用技巧 >MyBatis動態SQL(使用)整理

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 查詢條件

根據輸入的學生資訊進行條件檢索

  1. 當只輸入使用者名稱時, 使用使用者名稱進行模糊檢索;
  2. 當只輸入性別時, 使用性別進行完全匹配
  3. 當用戶名和性別都存在時, 用這兩個條件進行查詢匹配查詢

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 查詢條件

根據輸入的學生資訊進行條件檢索。

  1. 當只輸入使用者名稱時, 使用使用者名稱進行模糊檢索;
  2. 當只輸入性別時, 使用性別進行完全匹配
  3. 當用戶名和性別都存在時, 用這兩個條件進行查詢匹配查詢

不使用 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>

更改後的查詢結果如下