MyBatis 动态SQL详解
MyBatis 的动态SQL是其核心特性之一,允许基于条件动态构建SQL语句,避免了大量重复的SQL代码。
一、核心动态SQL标签
1. <if> 标签 - 条件判断
xml
<select id="findUsers" resultType="User">
SELECT * FROM users
WHERE 1=1
<if test="name != null and name != ''">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null">
AND email LIKE CONCAT('%', #{email}, '%')
</if>
</select>
2. <choose>, <when>, <otherwise> - 多条件选择
xml
<select id="findActiveUsers" resultType="User">
SELECT * FROM users
WHERE
<choose>
<when test="status == 'active'">
status = 'ACTIVE'
</when>
<when test="status == 'inactive'">
status = 'INACTIVE'
</when>
<otherwise>
status = 'PENDING'
</otherwise>
</choose>
</select>
3. <where> 标签 - 智能处理WHERE子句
xml
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
<if test="email != null">
AND email LIKE #{email}
</if>
</where>
ORDER BY id
</select>
特点:自动删除第一个多余的AND或OR,如果所有条件都不满足,则不生成WHERE子句。
4. <set> 标签 - 更新语句
xml
<update id="updateUser">
UPDATE users
<set>
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
<if test="email != null">email = #{email},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
</set>
WHERE id = #{id}
</update>
特点:自动删除末尾多余的逗号。
5. <trim> 标签 - 自定义截取
xml
<!-- 替代<where> -->
<select id="findUsers" resultType="User">
SELECT * FROM users
<trim prefix="WHERE" prefixOverrides="AND |OR ">
<if test="name != null">AND name = #{name}</if>
<if test="age != null">OR age = #{age}</if>
</trim>
</select>
<!-- 替代<set> -->
<update id="updateUser">
UPDATE users
<trim prefix="SET" suffixOverrides=",">
<if test="name != null">name = #{name},</if>
<if test="age != null">age = #{age},</if>
</trim>
WHERE id = #{id}
</update>
6. <foreach> 标签 - 循环遍历
xml
<!-- IN查询 -->
<select id="findUsersByIds" resultType="User">
SELECT * FROM users
WHERE id IN
<foreach item="id" index="index" collection="ids"
open="(" separator="," close=")">
#{id}
</foreach>
</select>
<!-- 批量插入 -->
<insert id="batchInsertUsers">
INSERT INTO users (name, age, email) VALUES
<foreach item="user" collection="list" separator=",">
(#{user.name}, #{user.age}, #{user.email})
</foreach>
</insert>
<!-- 批量更新(MySQL) -->
<update id="batchUpdateUsers">
<foreach item="user" collection="list" separator=";">
UPDATE users
SET name = #{user.name}, age = #{user.age}
WHERE id = #{user.id}
</foreach>
</update>
7. <bind> 标签 - 变量绑定
xml
<select id="searchUsers" resultType="User">
<!-- 创建变量并在OGNL表达式中使用 -->
<bind name="pattern" value="'%' + name + '%'" />
<bind name="emailPattern" value="email + '%'" />
SELECT * FROM users
WHERE
name LIKE #{pattern}
AND email LIKE #{emailPattern}
</select>
二、OGNL表达式
MyBatis动态SQL中使用的表达式语言:
xml
<!-- 复杂条件判断 -->
<if test="(name != null and name != '') or (age != null and age > 0)">
AND status = 'ACTIVE'
</if>
<!-- 调用Java方法 -->
<if test="@com.example.StringUtils@isNotEmpty(email)">
AND email = #{email}
</if>
<!-- 集合判断 -->
<if test="ids != null and ids.size() > 0">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
三、实战应用示例
1. 复杂搜索查询
xml
<select id="searchWithPagination" resultType="User">
SELECT * FROM users
<where>
<!-- 姓名模糊搜索 -->
<if test="name != null and name != ''">
<bind name="namePattern" value="'%' + name + '%'" />
AND (first_name LIKE #{namePattern} OR last_name LIKE #{namePattern})
</if>
<!-- 年龄范围 -->
<if test="minAge != null">
AND age >= #{minAge}
</if>
<if test="maxAge != null">
AND age <= #{maxAge}
</if>
<!-- 状态多选 -->
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status" open="(" close=")" separator=",">
#{status}
</foreach>
</if>
<!-- 注册时间范围 -->
<if test="startDate != null and endDate != null">
AND register_time BETWEEN #{startDate} AND #{endDate}
</if>
</where>
<!-- 动态排序 -->
<choose>
<when test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
<if test="orderDirection != null and orderDirection != ''">
${orderDirection}
</if>
</when>
<otherwise>
ORDER BY id DESC
</otherwise>
</choose>
<!-- 分页(MySQL) -->
<if test="offset != null and limit != null">
LIMIT #{offset}, #{limit}
</if>
</select>
2. 批量操作优化
xml
<!-- 批量插入(MySQL的ON DUPLICATE KEY UPDATE) -->
<insert id="batchInsertOrUpdate">
INSERT INTO products (id, name, price, stock) VALUES
<foreach item="product" collection="list" separator=",">
(#{product.id}, #{product.name}, #{product.price}, #{product.stock})
</foreach>
ON DUPLICATE KEY UPDATE
name = VALUES(name),
price = VALUES(price),
stock = stock + VALUES(stock)
</insert>
<!-- 批量删除 -->
<delete id="batchDelete">
DELETE FROM users
WHERE id IN
<foreach item="id" collection="ids" open="(" close=")" separator=",">
#{id}
</foreach>
AND status = 'INACTIVE'
</delete>
3. 动态表名和列名
xml
<sql id="dynamicTable">
<!-- 根据年份分表 -->
<choose>
<when test="year == 2023">orders_2023</when>
<when test="year == 2024">orders_2024</when>
<otherwise>orders</otherwise>
</choose>
</sql>
<select id="findOrdersByYear" resultType="Order">
SELECT * FROM
<include refid="dynamicTable"/>
WHERE user_id = #{userId}
</select>
四、注解方式动态SQL
java
// 使用@SelectProvider注解
@SelectProvider(type = UserSqlProvider.class, method = "buildSearchSql")
List<User> searchUsers(@Param("name") String name,
@Param("status") String status);
// SQL提供者类
public class UserSqlProvider {
public String buildSearchSql(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("users");
if (params.get("name") != null) {
WHERE("name = #{name}");
}
if (params.get("status") != null) {
WHERE("status = #{status}");
}
ORDER_BY("id DESC");
}}.toString();
}
}
// 或者使用字符串拼接方式
public class UserSqlProvider {
public String buildDynamicSql(Map<String, Object> params) {
String sql = "SELECT * FROM users WHERE 1=1";
if (params.get("name") != null) {
sql += " AND name = #{name}";
}
if (params.get("minAge") != null) {
sql += " AND age >= #{minAge}";
}
return sql;
}
}
五、高级技巧
1. 使用<script>包裹动态SQL
xml
<!-- 在注解中使用动态SQL -->
@Update("<script>" +
"UPDATE users " +
"<set>" +
" <if test='name != null'>name = #{name},</if>" +
" <if test='age != null'>age = #{age},</if>" +
"</set>" +
"WHERE id = #{id}" +
"</script>")
void updateUserSelective(User user);
2. 动态include SQL片段
xml
<!-- 定义可重用的SQL片段 -->
<sql id="userColumns">
id, name, age, email,
<if test="includeSensitive == true">
phone, address
</if>
</sql>
<!-- 使用 -->
<select id="getUser" resultType="map">
SELECT
<include refid="userColumns">
<property name="includeSensitive" value="true"/>
</include>
FROM users WHERE id = #{id}
</select>
3. 条件中的类型处理
xml
<select id="findByConditions" resultType="User">
SELECT * FROM users
<where>
<!-- 字符串判断 -->
<if test='name != null and name != ""'>
AND name = #{name}
</if>
<!-- 数字判断 -->
<if test="age != null">
AND age = #{age}
</if>
<!-- 布尔判断 -->
<if test="active != null">
AND is_active = #{active}
</if>
<!-- 集合/数组判断 -->
<if test="ids != null and ids.length > 0">
AND id IN
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</if>
</where>
</select>
六、性能优化建议
- 避免过度使用动态SQL:简单的条件判断可使用数据库函数
- 合理使用索引:确保动态生成的SQL能利用索引
- 分页优化:大数据量时使用延迟加载
- 缓存策略:合理配置MyBatis二级缓存
- 批量操作 :使用
<foreach>进行批量操作,减少数据库交互
七、常见问题解决
1. 参数为0时判断失效
xml
<!-- 错误 -->
<if test="status != null">
<!-- 当status=0时,条件不生效 -->
</if>
<!-- 正确 -->
<if test="status != null or status == 0">
AND status = #{status}
</if>
2. 特殊字符转义
xml
<!-- 使用CDATA或转义 -->
<if test="type != null and type != ''">
AND type = #{type}
<!-- 小于号要转义 -->
AND age < 18
</if>
<!-- 或者使用CDATA -->
<if test="value != null">
AND amount <![CDATA[ < ]]> #{maxValue}
</if>
3. 动态排序安全问题
xml
<!-- 不安全:直接使用${} -->
ORDER BY ${orderBy}
<!-- 安全方案:白名单验证 -->
ORDER BY
<choose>
<when test="orderBy == 'name'">name</when>
<when test="orderBy == 'age'">age</when>
<when test="orderBy == 'create_time'">create_time</when>
<otherwise>id</otherwise>
</choose>
总结
MyBatis动态SQL提供了强大而灵活的SQL构建能力,通过合理使用各种标签,可以:
- 减少大量重复SQL代码
- 提高代码可维护性
- 实现复杂的业务查询逻辑
- 保证SQL注入安全(正确使用时)
关键是要掌握各种标签的使用场景和组合方式,同时注意性能优化和安全问题。