MyBatis動(dòng)態(tài)標(biāo)簽詳解與應(yīng)用實(shí)踐舉例
引言
MyBatis作為一款優(yōu)秀的持久層框架,其最強(qiáng)大的特性之一就是動(dòng)態(tài)SQL功能。在實(shí)際開(kāi)發(fā)中,我們經(jīng)常需要根據(jù)不同的業(yè)務(wù)條件構(gòu)建靈活的SQL查詢,比如多條件搜索、動(dòng)態(tài)更新字段、批量操作等場(chǎng)景。如果使用傳統(tǒng)的JDBC方式手動(dòng)拼接SQL,不僅代碼冗余、維護(hù)困難,還容易出現(xiàn)SQL語(yǔ)法錯(cuò)誤和注入風(fēng)險(xiǎn)。
MyBatis通過(guò)提供一組功能強(qiáng)大的動(dòng)態(tài)標(biāo)簽,讓我們能夠以聲明式的方式動(dòng)態(tài)構(gòu)建SQL語(yǔ)句,極大地提升了開(kāi)發(fā)效率和代碼質(zhì)量。這些標(biāo)簽基于OGNL表達(dá)式實(shí)現(xiàn),能夠根據(jù)運(yùn)行時(shí)條件智能生成SQL,避免了手動(dòng)拼接SQL的復(fù)雜性和風(fēng)險(xiǎn)。
核心標(biāo)簽詳解
1. <if> 標(biāo)簽
功能說(shuō)明:<if>標(biāo)簽是最基礎(chǔ)的條件判斷標(biāo)簽,根據(jù)test屬性中的OGNL表達(dá)式結(jié)果決定是否包含對(duì)應(yīng)的SQL片段。
完整示例:
<select id="findUserByCondition" resultType="User">
SELECT * FROM user
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
</select>注意事項(xiàng):
- 數(shù)值類型判斷時(shí),只需要判斷是否為null,不要判斷是否等于空字符串
- 字符串類型需要同時(shí)判斷null和空字符串
- 多個(gè)if標(biāo)簽拼接時(shí)要注意AND/OR的處理,建議配合where標(biāo)簽使用
2. <where> 標(biāo)簽
功能說(shuō)明:<where>標(biāo)簽智能處理WHERE子句,只有在內(nèi)部有條件時(shí)才插入WHERE關(guān)鍵字,并且自動(dòng)去除開(kāi)頭多余的AND或OR,避免了"WHERE 1=1"這種寫法。
完整示例:
<select id="findUserSmart" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="status != null">
AND status = #{status}
</if>
</where>
</select>注意事項(xiàng):
- 只能去除開(kāi)頭的AND/OR,無(wú)法處理末尾的多余字符
- 當(dāng)所有條件都為空時(shí),不會(huì)生成WHERE子句
- 內(nèi)部的條件建議都加上AND或OR前綴
3. <choose>、<when>、<otherwise> 標(biāo)簽
功能說(shuō)明:
這組標(biāo)簽類似于Java中的switch-case語(yǔ)句,實(shí)現(xiàn)多條件分支選擇。按順序判斷when條件,一旦某個(gè)條件滿足就執(zhí)行對(duì)應(yīng)分支,都不滿足時(shí)執(zhí)行otherwise。
完整示例:
<select id="findUserByPriority" resultType="User">
SELECT * FROM user
<where>
<choose>
<when test="username != null and username != ''">
AND username = #{username}
</when>
<when test="email != null and email != ''">
AND email = #{email}
</when>
<otherwise>
AND status = 'ACTIVE'
</otherwise>
</choose>
</where>
</select>注意事項(xiàng):
- when條件是按順序判斷的,只執(zhí)行第一個(gè)滿足條件的分支
- otherwise是可選的,類似于default分支
- 適合互斥條件的場(chǎng)景,避免多個(gè)if嵌套
4. <set> 標(biāo)簽
功能說(shuō)明:<set>標(biāo)簽專門用于UPDATE語(yǔ)句,智能處理SET關(guān)鍵字,自動(dòng)去除末尾多余的逗號(hào)。
完整示例:
<update id="updateUserSelective" parameterType="User">
UPDATE user
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="password != null and password != ''">
password = #{password},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null">
email = #{email},
</if>
<if test="status != null">
status = #{status},
</if>
</set>
WHERE id = #{id}
</update>注意事項(xiàng):
- 每個(gè)if條件后都要加逗號(hào)
- 至少要有一個(gè)字段被更新,否則SQL語(yǔ)法錯(cuò)誤
- WHERE條件不要放在set標(biāo)簽內(nèi)部
5. <trim> 標(biāo)簽
功能說(shuō)明:<trim>標(biāo)簽是最靈活的字符串處理標(biāo)簽,可以自定義添加或刪除SQL片段的前綴、后綴。
屬性說(shuō)明:
- prefix:添加前綴
- suffix:添加后綴
- prefixOverrides:去除指定的前綴字符
- suffixOverrides:去除指定的后綴字符
完整示例:
<!-- 替代where標(biāo)簽 -->
<select id="findUserByTrim" resultType="User">
SELECT * FROM user
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="username != null">
AND username = #{username}
</if>
<if test="age != null">
OR age = #{age}
</if>
</trim>
</select>
<!-- 替代set標(biāo)簽 -->
<update id="updateUserByTrim" parameterType="User">
UPDATE user
<trim prefix="SET" suffixOverrides=",">
<if test="username != null">username = #{username},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
</trim>
WHERE id = #{id}
</update>注意事項(xiàng):
- prefixOverrides中的多個(gè)字符之間要用空格分隔(如"AND |OR ")
- 可以實(shí)現(xiàn)where和set標(biāo)簽的所有功能
- 注意空格的處理,避免SQL語(yǔ)法錯(cuò)誤
6. <foreach> 標(biāo)簽
功能說(shuō)明:<foreach>標(biāo)簽用于遍歷集合,常用于IN查詢、批量插入、批量刪除等場(chǎng)景。
屬性說(shuō)明:
- collection:集合參數(shù)名
- item:當(dāng)前迭代元素的變量名
- index:當(dāng)前迭代索引(可選)
- open:遍歷開(kāi)始時(shí)的字符串
- close:遍歷結(jié)束時(shí)的字符串
- separator:元素之間的分隔符
完整示例:
<!-- IN查詢 -->
<select id="findUserByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsertUsers">
INSERT INTO user (username, age, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.age}, #{user.email})
</foreach>
</insert>
<!-- 批量刪除 -->
<delete id="batchDeleteUsers">
DELETE FROM user
WHERE id IN
<foreach collection="idList" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>注意事項(xiàng):
- List集合默認(rèn)collection名為"list"或"collection"
- 數(shù)組默認(rèn)collection名為"array"
- 使用@Param注解可以自定義collection名稱
- 注意批量操作的數(shù)據(jù)量,避免SQL過(guò)長(zhǎng)
7. <bind> 標(biāo)簽
功能說(shuō)明:<bind>標(biāo)簽用于創(chuàng)建一個(gè)變量并綁定到上下文中,常用于預(yù)處理參數(shù),比如模糊查詢的通配符拼接。
完整示例:
<select id="findUserByPattern" resultType="User">
<bind name="pattern" value="'%' + username + '%'" />
SELECT * FROM user
WHERE username LIKE #{pattern}
</select>
<select id="findUserByDateRange" resultType="User">
<bind name="startDate" value="startDate + ' 00:00:00'" />
<bind name="endDate" value="endDate + ' 23:59:59'" />
SELECT * FROM user
WHERE create_time BETWEEN #{startDate} AND #{endDate}
</select>注意事項(xiàng):
- 可以使用OGNL表達(dá)式進(jìn)行復(fù)雜的計(jì)算
- 創(chuàng)建的變量只在當(dāng)前SQL語(yǔ)句中有效
- 適合復(fù)雜的字符串處理和日期處理
8. <sql> 和 <include> 標(biāo)簽
功能說(shuō)明:
這組標(biāo)簽用于定義可重用的SQL片段,提高代碼復(fù)用性和維護(hù)性。
完整示例:
<!-- 定義可復(fù)用的SQL片段 -->
<sql id="userBaseColumns">
id, username, password, age, email, status, create_time, update_time
</sql>
<sql id="userBaseQuery">
SELECT <include refid="userBaseColumns" />
FROM user
</sql>
<!-- 使用SQL片段 -->
<select id="findAllUsers" resultType="User">
<include refid="userBaseQuery" />
WHERE status = 'ACTIVE'
</select>
<select id="findUserById" resultType="User">
<include refid="userBaseQuery" />
WHERE id = #{id}
</select>
<!-- 帶參數(shù)的SQL片段 -->
<sql id="userCondition">
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
</sql>
<select id="searchUsers" resultType="User">
<include refid="userBaseQuery" />
<where>
<include refid="userCondition" />
</where>
</select>注意事項(xiàng):
- SQL片段可以包含動(dòng)態(tài)標(biāo)簽
- 可以嵌套使用include標(biāo)簽
- 合理提取公共SQL片段,避免過(guò)度抽象
綜合案例
用戶查詢綜合案例
下面是一個(gè)包含多個(gè)動(dòng)態(tài)標(biāo)簽組合使用的完整示例,展示了實(shí)際開(kāi)發(fā)中的應(yīng)用場(chǎng)景:
<!-- 用戶綜合查詢接口 -->
<select id="findUsersComprehensive" resultType="User">
SELECT
u.id, u.username, u.age, u.email, u.status,
u.create_time, u.update_time
FROM user u
<where>
<!-- 基礎(chǔ)條件 -->
<if test="username != null and username != ''">
AND u.username LIKE CONCAT('%', #{username}, '%')
</if>
<!-- 狀態(tài)多選 -->
<if test="statusList != null and statusList.size() > 0">
AND u.status IN
<foreach collection="statusList" item="status" open="(" separator="," close=")">
#{status}
</foreach>
</if>
<!-- 年齡范圍 -->
<if test="minAge != null">
AND u.age >= #{minAge}
</if>
<if test="maxAge != null">
AND u.age <= #{maxAge}
</if>
<!-- 創(chuàng)建時(shí)間范圍 -->
<if test="startTime != null">
AND u.create_time >= #{startTime}
</if>
<if test="endTime != null">
AND u.create_time <= #{endTime}
</if>
<!-- 優(yōu)先級(jí)選擇 -->
<choose>
<when test="email != null and email != ''">
AND u.email = #{email}
</when>
<when test="phone != null and phone != ''">
AND u.phone = #{phone}
</when>
</choose>
<!-- ID列表 -->
<if test="userIds != null and userIds.size() > 0">
AND u.id IN
<foreach collection="userIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</where>
<!-- 動(dòng)態(tài)排序 -->
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
<if test="orderDirection != null and orderDirection != ''">
${orderDirection}
</if>
</if>
<!-- 分頁(yè) -->
<if test="pageSize != null and pageSize > 0">
LIMIT #{offset}, #{pageSize}
</if>
</select>
<!-- 用戶動(dòng)態(tài)更新接口 -->
<update id="updateUserSelective" parameterType="User">
UPDATE user
<set>
<if test="username != null and username != ''">
username = #{username},
</if>
<if test="password != null and password != ''">
password = #{password},
</if>
<if test="age != null">
age = #{age},
</if>
<if test="email != null and email != ''">
email = #{email},
</if>
<if test="phone != null and phone != ''">
phone = #{phone},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="updateTime != null">
update_time = #{updateTime},
</if>
</set>
WHERE id = #{id}
</update>
<!-- 批量插入用戶 -->
<insert id="batchInsertUsers" parameterType="java.util.List">
INSERT INTO user (username, password, age, email, phone, status, create_time, update_time)
VALUES
<foreach collection="list" item="user" separator=",">
(
#{user.username},
#{user.password},
#{user.age},
#{user.email},
#{user.phone},
#{user.status},
#{user.createTime},
#{user.updateTime}
)
</foreach>
</insert>最佳實(shí)踐
1. 動(dòng)態(tài)標(biāo)簽選擇策略
| 場(chǎng)景 | 推薦標(biāo)簽 | 說(shuō)明 |
|---|---|---|
| 簡(jiǎn)單條件判斷 | <if> | 最基礎(chǔ)的條件判斷 |
| 多條件組合查詢 | <where> + <if> | 智能處理WHERE和AND/OR |
| 互斥條件選擇 | <choose> + <when> | 避免多個(gè)if嵌套 |
| 動(dòng)態(tài)更新字段 | <set> + <if> | 自動(dòng)處理逗號(hào)和SET關(guān)鍵字 |
| 自定義字符串處理 | <trim> | 最靈活的字符串處理 |
| 集合遍歷操作 | <foreach> | IN查詢、批量操作 |
| 參數(shù)預(yù)處理 | <bind> | 模糊查詢、日期處理 |
| SQL片段復(fù)用 | <sql> + <include> | 提取公共SQL |
2. 性能優(yōu)化建議
減少動(dòng)態(tài)判斷開(kāi)銷:
<!-- 不推薦:過(guò)度動(dòng)態(tài) -->
<select id="findUser">
SELECT <if test="columns != null">${columns}</if> <if test="columns == null">*</if> FROM user
</select>
<!-- 推薦:明確字段 -->
<select id="findUser">
SELECT id, username, email FROM user
<where>
<if test="username != null">
AND username = #{username}
</if>
</where>
</select>批量操作優(yōu)化:
<!-- 推薦:分批次處理,避免SQL過(guò)長(zhǎng) -->
<insert id="batchInsertUsers">
INSERT INTO user (username, email) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.username}, #{user.email})
</foreach>
</insert>
<!-- Java代碼控制批次大小 -->
List<User> users = getUserList();
int batchSize = 500;
for (int i = 0; i < users.size(); i += batchSize) {
List<User> batch = users.subList(i, Math.min(i + batchSize, users.size()));
userMapper.batchInsertUsers(batch);
}3. 安全性注意事項(xiàng)
防止SQL注入:
<!-- 危險(xiǎn):直接拼接用戶輸入 -->
<select id="findUserDangerous">
SELECT * FROM user ORDER BY ${orderBy}
</select>
<!-- 安全:白名單校驗(yàn) -->
<select id="findUserSafe">
SELECT * FROM user
<choose>
<when test="orderBy == 'username'">ORDER BY username</when>
<when test="orderBy == 'create_time'">ORDER BY create_time</when>
<otherwise>ORDER BY id</otherwise>
</choose>
</select>避免全表操作風(fēng)險(xiǎn):
<!-- 危險(xiǎn):可能導(dǎo)致全表更新 -->
<update id="updateUserDangerous">
UPDATE user
<set>
<if test="status != null">status = #{status}</if>
</set>
<where>
<if test="id != null">id = #{id}</if>
</where>
</update>
<!-- 安全:確保WHERE條件 -->
<update id="updateUserSafe">
UPDATE user
<set>
status = #{status}
</set>
WHERE id = #{id}
</update>4. 代碼可維護(hù)性
提取公共SQL片段:
<!-- 定義公共字段 -->
<sql id="userCommonFields">
id, username, email, status, create_time
</sql>
<!-- 定義公共條件 -->
<sql id="userCommonCondition">
status = 'ACTIVE'
AND delete_flag = 0
</sql>
<!-- 使用公共片段 -->
<select id="findActiveUsers" resultType="User">
SELECT <include refid="userCommonFields" />
FROM user
WHERE <include refid="userCommonCondition" />
</select>注釋復(fù)雜邏輯:
<!-- 用戶查詢:支持多條件動(dòng)態(tài)組合
1. username:模糊查詢
2. statusList:多狀態(tài)選擇
3. 時(shí)間范圍:create_time區(qū)間
4. 排序:支持動(dòng)態(tài)排序字段
-->
<select id="findUsersComplex" resultType="User">
SELECT * FROM user
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status"
open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
</select>5. 參數(shù)校驗(yàn)建議
Java層校驗(yàn):
public List<User> findUsers(UserQuery query) {
// 參數(shù)校驗(yàn)
if (query == null) {
query = new UserQuery();
}
// 集合判空
if (CollectionUtils.isEmpty(query.getStatusList())) {
return Collections.emptyList();
}
// 分頁(yè)參數(shù)校驗(yàn)
if (query.getPageSize() != null && query.getPageSize() > 100) {
query.setPageSize(100); // 限制最大頁(yè)數(shù)
}
return userMapper.findUsers(query);
}XML層安全判斷:
<select id="findUsersSafe" resultType="User">
SELECT * FROM user
<where>
<!-- 雙重判空保護(hù) -->
<if test="username != null and username != ''">
AND username = #{username}
</if>
<!-- 集合安全判斷 -->
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status"
open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
</select>總結(jié)
MyBatis的動(dòng)態(tài)標(biāo)簽為開(kāi)發(fā)者提供了強(qiáng)大而靈活的SQL構(gòu)建能力,通過(guò)合理使用這些標(biāo)簽,我們可以:
- 提升開(kāi)發(fā)效率:避免手動(dòng)拼接SQL的繁瑣工作
- 增強(qiáng)代碼可讀性:聲明式的SQL構(gòu)建更直觀易懂
- 保證SQL安全性:通過(guò)預(yù)編譯防止SQL注入
- 提高維護(hù)性:SQL與業(yè)務(wù)邏輯分離,便于維護(hù)
在實(shí)際應(yīng)用中,需要根據(jù)具體場(chǎng)景選擇合適的動(dòng)態(tài)標(biāo)簽,并遵循性能優(yōu)化和安全性的最佳實(shí)踐,避免過(guò)度使用動(dòng)態(tài)標(biāo)簽導(dǎo)致的復(fù)雜性問(wèn)題。
參考資料:
推薦閱讀:
- 《MyBatis從入門到精通》- 動(dòng)態(tài)SQL章節(jié)
- 《深入理解MyBatis技術(shù)原理與實(shí)戰(zhàn)》- SQL映射詳解
- MyBatis-Plus官方文檔 - 動(dòng)態(tài)SQL增強(qiáng)功能
到此這篇關(guān)于MyBatis動(dòng)態(tài)標(biāo)簽詳解與應(yīng)用實(shí)踐的文章就介紹到這了,更多相關(guān)MyBatis動(dòng)態(tài)標(biāo)簽內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Kotlin 高階函數(shù) 與 Lambda 表達(dá)式
這篇文章主要介紹了詳解Kotlin 高階函數(shù) 與 Lambda 表達(dá)式的相關(guān)資料,需要的朋友可以參考下2017-06-06
在Spring中利用@Order注解對(duì)bean和依賴進(jìn)行排序
在Spring框架中,@Order是一個(gè)經(jīng)常被忽視但非常重要的注解,在項(xiàng)目開(kāi)發(fā)中,當(dāng)我們需要維護(hù)bean的特定順序或者存在許多相同類型的bean時(shí),這個(gè)注解就發(fā)揮了作用,這篇文章講的就是如何利用@Order注解對(duì)bean和依賴進(jìn)行排序,需要的朋友可以參考下2023-11-11
Java日志框架打印應(yīng)用程序日志代碼的執(zhí)行情況分析
在配置INFO日志級(jí)別時(shí),日志器(logger)中debug級(jí)的日志代碼仍會(huì)被執(zhí)行,只是是否輸出取決于配置的日志級(jí)別,本文基于Java 1.8、SLF4J 1.7.25和Log4j 2.20.0進(jìn)行實(shí)驗(yàn),詳述了日志框架處理日志代碼的機(jī)制,感興趣的朋友一起看看吧2024-10-10
iOS獲取AppIcon and LaunchImage''s name(app圖標(biāo)和啟動(dòng)圖片名字)
這篇文章主要介紹了iOS獲取AppIcon and LaunchImage's name(app圖標(biāo)和啟動(dòng)圖片名字)的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-08-08
最新springboot中必須要了解的自動(dòng)裝配原理
本文給大家介紹springboot中必須要了解的自動(dòng)裝配原理,spring-boot-dependencies:核心依賴都在父工程中,這個(gè)里面主要是管理項(xiàng)目的資源過(guò)濾及插件,本文對(duì)springboot自動(dòng)裝配原理給大家介紹的非常詳細(xì),需要的朋友參考下吧2022-05-05
Java實(shí)戰(zhàn)之利用POI生成Excel圖表
Apache POI是Java生態(tài)中處理Office文檔的核心工具,這篇文章主要為大家詳細(xì)介紹了如何在Excel中創(chuàng)建折線圖,柱狀圖,餅圖等常見(jiàn)圖表,需要的可以參考下2025-02-02

