if
导学
本节你将掌握 MyBatis 中最基础的动态 SQL 元素 if,理解它如何根据传入参数的条件动态拼接 SQL 片段。学习目标是:
- 掌握
test属性中 OGNL 表达式的写法 - 理解
if在动态查询中的典型用法 - 认识单独使用
if时存在的 AND/OR 拼接问题(为后续学习where做铺垫) - 能够独立编写基于
if的动态条件查询
定义
if 是 MyBatis 动态 SQL 中最基础的条件判断元素。它根据 test 属性中 OGNL 表达式的求值结果,决定是否将包裹的 SQL 片段包含到最终执行的 SQL 语句中。
在传统的 JDBC 编程中,开发者需要手动拼接 SQL 字符串:
String sql = "SELECT * FROM student WHERE 1=1";
if (name != null) {
sql += " AND name LIKE '%" + name + "%'"; // 丑陋的字符串拼接
}
if (major != null) {
sql += " AND major = '" + major + "'"; // SQL 注入风险!
}
这种写法不仅代码丑陋、难以维护,还存在严重的 SQL 注入安全隐患。MyBatis 的 if 元素将条件判断从 Java 代码中剥离到 XML 映射文件中,配合预编译参数占位符 #{ },从根本上解决了字符串拼接和 SQL 注入的问题。
适用位置与核心属性
if 元素只能嵌套在以下动态 SQL 父元素内部使用:where、set、trim、foreach,或作为 choose 的子元素 when 的底层实现。
| 属性 | 是否必填 | 说明 |
|---|---|---|
test | 是 | OGNL 表达式,返回布尔值。结果为 true 时包含该 SQL 片段,为 false 时忽略 |
核心原理
动态 SQL if 条件判断流程
OGNL 表达式求值流程
OGNL(Object-Graph Navigation Language)是一种强大的表达式语言。MyBatis 将传入的参数对象(可以是实体类、Map 或基本类型包装对象)放入 OGNL 上下文,然后对 test 属性中的表达式进行求值。常用的 OGNL 表达式写法包括:
| 表达式 | 含义 |
|---|---|
name != null | 属性不为 null |
name != '' | 属性不为空字符串 |
name != null and name != '' | 同时满足不为 null 且不为空字符串 |
score != null | 数值类型不为 null |
score >= 60 | 数值比较 |
student != null and student.name != null | 对象及其嵌套属性判断 |
完整示例
场景说明
乐途学院的学生管理系统需要提供一个动态组合查询功能:管理员可以只按姓名模糊查询,也可以只按专业精确查询,也可以同时按姓名和专业查询,也可以两个条件都不填(查询全部)。要求使用 if 元素实现动态条件拼接。
操作前的数据库表结构及初始数据
CREATE TABLE student (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20),
age INT,
major VARCHAR(20),
score DECIMAL(5,2)
);
INSERT INTO student (name, age, major, score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
当前表数据:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整的映射文件片段
StudentMapper.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.flying.mapper.StudentMapper">
<!-- 动态条件查询学生:使用 if 拼接 WHERE 条件 -->
<select id="findByCondition" resultType="Student"
parameterType="com.flying.entity.Student">
SELECT id, name, age, major, score
FROM student
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="major != null and major != ''">
AND major = #{major}
</if>
<if test="minScore != null">
AND score >= #{minScore}
</if>
</select>
</mapper>
Student.java(实体类)
package com.flying.entity;
public class Student {
private Integer id;
private String name;
private Integer age;
private String major;
private Double score;
private Double minScore; // 查询条件:最低分数
// getter 和 setter 省略
}
StudentMapper.java(接口)
package com.flying.mapper;
import com.flying.entity.Student;
import java.util.List;
public interface StudentMapper {
List<Student> findByCondition(Student condition);
}
实际执行结果
情况一:name 和 major 都传入
测试代码:
Student condition = new Student();
condition.setName("翔");
condition.setMajor("计算机科学");
List<Student> list = mapper.findByCondition(condition);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE 1=1 AND name LIKE CONCAT('%', ?, '%') AND major = ?
参数值: 翔, 计算机科学
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
情况二:只传入 name
测试代码:
Student condition = new Student();
condition.setName("白");
List<Student> list = mapper.findByCondition(condition);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE 1=1 AND name LIKE CONCAT('%', ?, '%')
参数值: 白
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
情况三:只传入 major
测试代码:
Student condition = new Student();
condition.setMajor("软件工程");
List<Student> list = mapper.findByCondition(condition);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE 1=1 AND major = ?
参数值: 软件工程
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
情况四:什么都不传入
测试代码:
Student condition = new Student();
List<Student> list = mapper.findByCondition(condition);
最终生成的 SQL 语句:
SELECT id, name, age, major, score FROM student WHERE 1=1
查询结果集:
| id | name | age | major | score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
分析
从四种情况的对比可以清晰看到 if 的工作机制:
- 条件满足时:OGNL 表达式求值为
true,if包裹的 SQL 片段被保留,最终 SQL 包含该条件 - 条件不满足时:OGNL 表达式求值为
false,if包裹的 SQL 片段被丢弃,最终 SQL 不包含该条件 - 1=1 的妥协:示例中使用了
WHERE 1=1这种技巧来避免第一个条件前没有 AND 的问题。如果没有1=1,当只有major条件满足时,SQL 会变成WHERE AND major = ?,这是语法错误的。这种写法虽然能工作,但不够优雅,下一节将学习where元素来彻底解决此问题。
易错场景 / 常见误区
| 误区 | 错误示例 | 后果 | 正解 |
|---|---|---|---|
只判断 != null 忽略空字符串 | <if test="name != null"> | 传入空字符串 "" 时,会拼接 name LIKE '%%',查询无意义 | <if test="name != null and name != ''"> |
在 test 中直接使用 #{ } | <if test="#{name} != null"> | 语法错误,test 中应直接写属性名 | <if test="name != null"> |
| 数值类型与空字符串比较 | <if test="age != ''"> | age 是 Integer 类型,与字符串比较可能触发类型转换异常 | 数值类型只判断 age != null 即可 |
| 忘记处理第一个条件的 AND | 没有 WHERE 1=1 | 当只有后面的条件满足时,SQL 变成 WHERE AND ...,语法错误 | 使用 WHERE 1=1 过渡,或改用 where 元素 |
| OGNL 中使用了 Java 保留字 | <if test="class != null"> | class 是 OGNL 保留字,解析失败 | 使用 _class 或改用其他属性名 |
面试考点
Q1:MyBatis 的 if 元素中 test 属性使用的是什么表达式语言?
A:使用的是 OGNL(Object-Graph Navigation Language)表达式语言。MyBatis 将参数对象放入 OGNL 上下文,由 OGNL 引擎对
test表达式进行求值,返回布尔结果决定是否包含该 SQL 片段。
Q2:为什么单独使用 if 时通常要写 WHERE 1=1?
A:因为当多个
if条件动态拼接时,无法确定哪个条件会第一个满足。如果第一个满足的条件前带有AND,最终 SQL 会变成WHERE AND ...,导致语法错误。WHERE 1=1作为一个永真条件,确保后续所有满足的条件前都可以安全地加上AND。但这种写法不够优雅,生产环境中推荐使用where元素来自动处理。
Q3:if 的 test 中能否使用 && 和 ||?
A:不能直接使用 Java 的
&&和||。OGNL 中使用and/&&(部分版本支持)和or/||,但最稳妥的写法是使用英文单词and和or,例如name != null and name != ''。
Q4:以下写法有什么问题?
<if test="name != null and name != ''"> AND name = '%#{name}%' </if>
A:存在两个问题:①
#{ }是预编译参数占位符,不应该放在单引号内部,否则会被当作字符串字面量处理;② 模糊查询应该使用CONCAT('%', #{name}, '%')或数据库特定的字符串拼接函数,而不是直接拼接百分号。正确写法是name LIKE CONCAT('%', #{name}, '%')。
小结
if 是 MyBatis 动态 SQL 的基石,它通过 OGNL 表达式实现了条件化的 SQL 片段拼接,彻底告别了 JDBC 时代丑陋的字符串拼接和 SQL 注入风险。但单独使用 if 时,开发者需要手动处理 WHERE 关键字和多余的 AND/OR,通常借助 WHERE 1=1 这种技巧来规避。这种妥协方案虽然能工作,但不够优雅。
下一节我们将学习 where 元素,它是专门为解决 if 的 WHERE 和 AND 拼接问题而生的智能包装器,让动态查询更加简洁和专业。
下一章引子
当你掌握了 if 的基础用法后,一定会对 WHERE 1=1 这种妥协写法感到不满。where 元素正是 MyBatis 为解决这个问题提供的优雅方案——它能自动插入 WHERE 关键字,并智能移除多余的 AND 或 OR。继续阅读,让你的动态查询代码更加专业。