文章目录
- 前言
- [一、MyBatis 参数传递与单条件查询](#一、MyBatis 参数传递与单条件查询)
-
- [1、参数占位符 : #{} 与 {}](#{} 与 {})
- [2、parameterType 参数类型](#2、parameterType 参数类型)
- 3、特殊字符处理
- 二、单条件查询案例实现
-
- [1、第一步 : Mapper 接口定义方法](#1、第一步 : Mapper 接口定义方法)
- [2、第二步 : SQL 映射文件编写 SQL](#2、第二步 : SQL 映射文件编写 SQL)
- [3、第三步 : 测试类验证](#3、第三步 : 测试类验证)
- 三、多条件查询
- 四、多条件查询案例实现
-
- [1、Mapper 接口](#1、Mapper 接口)
- [2、SQL 映射文件](#2、SQL 映射文件)
- 3、测试类验证
- 五、多条件动态查询
-
- [1、if 标签 : 条件判断](#1、if 标签 : 条件判断)
- [2、if 的弊端与恒等式方案](#2、if 的弊端与恒等式方案)
- [3、where 标签 : 推荐方案](#3、where 标签 : 推荐方案)
- [4、Mapper 接口](#4、Mapper 接口)
- 5、测试方法
- 六、单条件动态查询
-
- [1、choose , when , otherwise 标签](#1、choose , when , otherwise 标签)
- [2、Mapper 接口函数](#2、Mapper 接口函数)
- [3、SQL 映射文件](#3、SQL 映射文件)
- 4、测试类验证
- 七、注意事项与避坑
- 总结
前言
Mybatis 的数据查询需求 由浅入深 可以分为四种 :
- 单条件查询 ,
- 多条件查询 ,
- 多条件动态条件查询 ,
- 单条件动态条件查询 ;
本文以 mybatis-demo 项目中的 BrandMapper 接口与 BrandMapper.xml 映射文件为载体 , 依次实现这四种查询 , 并把过程中涉及的 参数传递 , 参数占位符 , 特殊字符处理 , 动态 SQL 标签等核心知识一并讲透 ;
四种查询方式一览 :
- 单条件查询 : 条件固定只有一个 , 典型场景是根据 id 查看详情 , 核心知识点是参数占位符 ;
- 多条件查询 : 多个条件用 and 组合查询 , 核心知识点是三种参数传递方式 ;
- 多条件动态查询 : 条件可选可缺 , 核心知识点是 if 与 where 标签 ;
- 单条件动态查询 : 多个条件只按一个查 , 核心知识点是 choose , when , otherwise 标签 ;
一、MyBatis 参数传递与单条件查询
单条件查询的典型需求是 : 根据数据库表的 id 字段 查询对应的 Brand 数据 , SQL 里的 id 显然不能写死 , 于是引出 MyBatis 的参数传递机制 ;
1、参数占位符 : #{} 与 ${}
在 Mapper 接口中定义 Brand selectById(int id); 接口函数 , 对应的 SQL 映射文件中需要使用 占位符 接收该 接口函数 ;
MyBatis 提供两种参数占位符 : 写法相似 , 含义完全不同 ;
- #{} 预编译占位符 : 执行 SQL 时会先把
#{}替换为 JDBC 的?号 , 参数值随后通过setXxx()方法安全传入 , 底层走 PreparedStatement 预编译 , 可以防止 SQL 注入 , 绝大多数场景都使用它 ; - ${} 拼接占位符 : 直接把参数值当作字符串拼接进 SQL 再编译执行 , 存在 SQL 注入风险 , 仅用于动态表名 , 动态列名等 SQL 结构本身不固定的场景 ;
两者对比 :
| 对比项 | #{} | ${} |
|---|---|---|
| 底层原理 | 替换为 ? 号 , 预编译后通过 setXxx 设置参数值 | 参数值原样拼接进 SQL 字符串后编译 |
| SQL 注入 | 可以防止 | 存在风险 |
| 传参表现 | 值被当作一个完整的参数 , 不参与 SQL 语法 | 值直接参与 SQL 语法构成 |
| 使用场景 | 传递参数值 ( 绝大多数场景 ) | 动态表名 , 动态列名 ( 如分表 tb_brand_2024 ) |
直观示例 : 假设要查询 id 为 1 的品牌 , 两种占位符在 SQL 中的写法与最终执行效果完全不同 :
xml
<!-- #{} 预编译占位符 : 最终执行的是预编译 SQL , 参数安全传入 -->
<select id="selectById" resultMap="brandResultMap">
select * from tb_brand where id = #{id};
</select>
<!-- 实际执行 : select * from tb_brand where id = ? ( 参数 1 通过 setInt 传入 ) -->
<!-- ${} 拼接占位符 : 参数值直接拼进 SQL 字符串 -->
<select id="selectById" resultMap="brandResultMap">
select * from tb_brand where id = ${id};
</select>
<!-- 实际执行 : select * from tb_brand where id = 1 ( 值原样拼接 , 无预编译 ) -->
核心区别一句话总结 :
#{}是 " 占个坑 , 参数后补 " , 安全防注入 ;${}是 " 直接填值 " , 灵活但有风险 , 只在 动态表名 / 列名等 SQL 结构不固定的场景才用 ;
2、parameterType 参数类型
<select> 标签的 parameterType 属性用于声明入参类型 , 例如 parameterType="int" ; 由于 MyBatis 能 根据接口方法签名自动推断参数类型 , 该属性可以省略不写 ;
3、特殊字符处理
SQL 中出现 < , > 这类字符时会与 XML 语法冲突 , < 是标签起始符 , 直接书写会破坏 XML 结构导致解析报错 ; 解决方式有两种 :
特殊字符的两种处理方式 :
- 转义字符 : 用 XML 实体替代 ,
<代替<,>代替>,&代替&, 例如where ordered < 15; - CDATA 区 : 用
<![CDATA[ ... ]]>包住含特殊字符的 SQL 片段 , 区内内容 XML 不做解析 , 原样保留 ;
xml
<!-- 转义字符示例 : 查询排序值小于 15 的品牌 -->
<select id="selectByOrdered" resultMap="brandResultMap">
select * from tb_brand where ordered < #{ordered};
</select>
<!-- CDATA 区示例 : 效果等价 , 写法更直观 -->
<select id="selectByOrdered" resultMap="brandResultMap">
select * from tb_brand where <![CDATA[ ordered < ]]> #{ordered};
</select>
说明 : 只有 < 存在语法冲突必须处理 , > 可以直接书写 , 习惯上会一并处理 ;
二、单条件查询案例实现
有了 参数占位符 的知识铺垫 , 根据 id 查询品牌就可以按 " 接口方法 + SQL 映射 + 测试验证 " 三步落地 ;
1、第一步 : Mapper 接口定义方法
在 BrandMapper.java 接口中 , 定义 带参数的 接口函数 ;
java
/**
* 查看详情:根据Id查询
* <p>
* 【参数传递原理】
* 方法只有一个参数时 , SQL 中直接用 #{id} 即可取到该参数的值:
* MyBatis 会把参数值传入 SQL , 将 #{id} 替换为占位符 ? ,
* 再通过 PreparedStatement.setInt(1, id) 安全设置参数(防 SQL 注入)。
* 说明:#{ } 里面写什么名字都可以(如 #{abc}) , 因为只有一个参数 ,
* MyBatis 早期版本不关心占位符名称 , 只按位置传值;
* 但为了可读性 , 建议与方法参数名保持一致。
* <p>
* 【为什么不用 parameterType 属性?】
* <select> 标签的 parameterType 属性用于声明入参类型(如 int、String) ,
* MyBatis 可以根据接口方法签名自动推断出来 , 所以该属性可以省略不写。
* <p>
* 返回值 Brand:查询到的单个品牌对象;id 不存在时返回 null。
*/
Brand selectById(int id);
接口方法的两个要点 :
- 返回值 Brand : 主键查询最多命中一行 , 封装成单个 Brand 对象返回 , id 不存在时返回 null ;
- 绑定规则 : XML 中
<mapper namespace>必须是接口全限定名org.example.mapper.BrandMapper,<select>的 id 必须与方法名selectById完全一致 ;
2、第二步 : SQL 映射文件编写 SQL
在 BrandMapper.xml 的 SQL 映射文件中 , 定义 " Brand selectById(int id) " 接口函数 对应的 SQL 脚本 ;
xml
<!--
======================================================================
【本条 SQL 涉及的核心知识点:参数占位符】
======================================================================
MyBatis 中有两种参数占位符:
1. #{ }(预编译占位符 ------ 推荐)
* 底层使用 PreparedStatement , #{ } 会先被替换为 JDBC 的 ? 占位符 ,
参数值后续通过 setXxx() 方法安全传入;
* 传入的值会被当作「一个完整的参数值」处理 , 不参与 SQL 语法拼接 ,
因此可以【防止 SQL 注入】(如传入 "1 or '1'='1'" 也只会被当成普通字符串)。
2. ${ }(字符串拼接占位符)
* 直接把参数值原样【拼接到 SQL 语句】中再编译执行;
* 存在【SQL 注入】风险 , 除非特殊场景否则不要用。
3. 使用时机:
* 传递参数值的时候:用 #{ }(安全 , 本条 SQL 采用);
* 表名或列名(SQL 结构本身)不固定的时候:只能用 ${ }
(因为表名/列名不能作为 ? 占位符参数传入 , 如动态分表 tb_brand_2024) ,
但要自己做好参数校验防注入。
【参数类型 parameterType】
<select> 标签的 parameterType 属性用于声明入参类型(如 int、String) ,
MyBatis 能根据接口方法签名自动推断 , 所以该属性【可以省略】不写。
【特殊字符处理】(SQL 中出现 < 、> 等特殊字符时使用)
XML 文件中 < 是标签起始符 , 直接写 < 会破坏 XML 结构导致解析报错 , 两种解决办法:
1. 转义字符:用 XML 实体替代 , 如 <(<)、>(>)、&(&)
例:where id < 10 → 实际 SQL:where id < 10
2. CDATA 区:<![CDATA[ ... ]]> 包住含特殊字符的 SQL 片段 ,
区内内容 XML 不做解析、原样保留 , 写什么符号都可以
例:<![CDATA[ where id < 10 ]]>
说明:只有 < 有语法冲突必须处理;> 可以直接写 , 但习惯上一并处理。
======================================================================
-->
<!--
查看详情:根据 Id 查询
id :必须与 BrandMapper 接口方法名 完全一致(绑定依据)
parameterType :入参类型为 int , 可省略(已省略) , MyBatis 自动推断
resultMap :沿用上面的 brandResultMap , 解决 brand_name/company_name
列名与属性名不一致的封装问题
#{id} :取出接口方法传入的参数值 , 替换为 ? 预编译执行 , 防 SQL 注入
执行结果 :一行数据封装成一个 BselectByIdrand 对象返回;id 不存在时返回 null
-->
<select id="selectById" resultMap="brandResultMap">
select *
from tb_brand where id = #{id};
</select>
SQL 的取参原理 : 方法只有一个参数时 , MyBatis 把参数值直接传入 SQL , 将 #{id} 替换为 ? 后通过 PreparedStatement.setInt() 安全设置 , 此时占位符里写什么名字都能取到值 ( 例如 #{abc} ) , 但为了可读性建议与方法形参同名 ;
3、第三步 : 测试类验证
编写单元测试方法 , 调用上述 BrandMapper 接口中的 selectById 接口函数 ;
java
/**
* 测试:查看详情(根据 Id 查询)
*/
@Test
public void testSelectById() throws IOException {
//接收参数
int id = 1;
//1. 加载核心配置文件 , 获取 SqlSessionFactory
String resource = "mybatis-config-mapper.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取 SqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取 Mapper 接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法 , 打印结果
Brand brand = brandMapper.selectById(id);
System.out.println(brand);
//5. 释放资源
sqlSession.close();
}
固定套路共 5 步 : 加载配置 , 获取 SqlSession , 获取代理对象 , 执行方法 , 释放资源 , 后续所有查询都复用这套流程 ;
执行结果 : 查询的 id = 1 的数据对象是 Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1} ;
dart
[DEBUG] 20:56:39.434 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection
[DEBUG] 20:56:39.686 [main] o.a.i.d.p.PooledDataSource - Created connection 959869407.
[DEBUG] 20:56:39.686 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@393671df]
[DEBUG] 20:56:39.689 [main] o.e.m.B.selectById - ==> Preparing: select * from tb_brand where id = ?;
[DEBUG] 20:56:39.714 [main] o.e.m.B.selectById - ==> Parameters: 1(Integer)
[DEBUG] 20:56:39.724 [main] o.e.m.B.selectById - <== Total: 1
Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1}
[DEBUG] 20:56:39.725 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@393671df]
[DEBUG] 20:56:39.725 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@393671df]
[DEBUG] 20:56:39.726 [main] o.a.i.d.p.PooledDataSource - Returned connection 959869407 to pool.
进程已结束 , 退出代码为 0

三、多条件查询
同时按照 " 当前状态 , 企业名称 , 品牌名称 " 组合筛选 Brand 数据 , 这就是多条件查询 ;
它涉及两个新问题 : 条件表达式怎么写 , 多个参数怎么传 ;
1、条件表达式的编写与连接
常用的条件表达式 :
- 等值比较 : 用
=精确匹配 , 例如status = #{status}; - 模糊匹配 : 用
like配合%通配符 , 例如company_name like #{companyName},%属于参数值的一部分 , 要在调用方拼好再传入 ; - 条件连接 : 多个条件用
and连接表示 " 且 " , 用or连接表示 " 或 " , 例如where status = #{status} and company_name like #{companyName};
2、三种参数传递方式
接口方法有多个参数时 , MyBatis 不会自动命名 , SQL 中的 #{xxx} 要能取到值 , 必须保证 xxx 与传入参数的名字对得上 , 由此产生三种传参方式 :
java
// 方式一 : 散装参数 , 每个参数前用 @Param 绑定占位符名称
List<Brand> selectByCondition(@Param("status") int status,
@Param("companyName") String companyName,
@Param("brandName") String brandName);
// 方式二 : 对象参数 , 占位符名称与实体类属性名对应
List<Brand> selectByCondition(Brand brand);
// 方式三 : Map 集合参数 , 占位符名称与 Map 的 key 对应
List<Brand> selectByCondition(Map map);
三种方式对比 :
| 对比项 | 散装参数 | 对象参数 | Map 集合参数 |
|---|---|---|---|
| 对齐要求 | @Param 名称与占位符一致 | 属性名与占位符一致 | key 与占位符一致 |
| 类型约束 | 有 , 编译期即可检查 | 有 , 编译期即可检查 | 无 , 写错 key 不报错只查空 |
| 优点 | 参数含义一目了然 | 结构清晰 , 符合面向对象习惯 | 灵活 , 不依赖实体类 |
| 缺点 | 参数多时方法签名冗长 | 条件与实体字段强绑定 | 键名靠手写 , 容易拼错 |
| 使用场景 | 参数少且固定 | 条件与实体属性基本对应 | 多表查询等无现成实体的场景 |
四、多条件查询案例实现
多条件查询沿用 " Mapper 接口 + SQL 映射 XML + 测试 " 三步走 , 本案例启用 Map 集合参数方式 , 另两种方式注释保留 ;
1、Mapper 接口
一个重要限制 : MyBatis 接口不支持方法重载 , SQL 定位靠 " namespace + 方法名 " , 重载方法名相同会导致 XML 中 id 冲突 , 所以 三种传参方式同一时刻只能启用一种 , 其余注释备查 ;
在 BrandMapper.java 中定义如下 接口函数 :
java
/**
* 条件查询:按「状态 status + 公司名称 companyName + 品牌名称 brandName」多条件查询
* <p>
* 【多参数接收的三种方式】
* MyBatis 对多个参数不会自动命名 , SQL 中 #{xxx} 要能取到值 ,
* 必须保证「xxx 与传入参数的名字对得上」 , 三种方式对号入座:
* 1. 散装参数:方法有多个参数时 , 每个参数前必须加 @Param("xxx") 注解 ,
* 括号里的名称与 SQL 占位符 #{xxx} 一致
* (需导入 org.apache.ibatis.annotations.Param);
* 2. 对象参数:直接传 Brand 对象 , #{xxx} 的 xxx 与对象的【属性名】一致即可取值;
* 3. Map 集合参数:传入 Map , #{xxx} 的 xxx 与 Map 的【key】一致即可取值。
* <p>
* 【模糊查询 like 的传参约定】
* % 通配符属于「参数值的一部分」 , #{} 只做安全占位、不会自动拼通配符 ,
* 所以调用方需先把参数处理成 "%关键字%" 再传入(见测试类 testSelectByCondition)。
* <p>
* 【为何另外两种方式注释保留?】
* Mapper 接口【不支持方法重载】:SQL 定位靠「namespace + 方法名(id)」 ,
* 重载方法名相同会导致 XML 中的 id 冲突 , 因此同一时刻只能启用一种 , 其余注释备查。
* <p>
* 返回值 List<Brand>:符合条件的品牌集合;查不到时返回空集合(不是 null)。
*/
// 方式一:散装参数 ------ 多参数时必须用 @Param("占位符名称") 逐个声明映射关系
//List<Brand> selectByCondition(@Param("status") int status, @Param("companyName") String companyName, @Param("brandName") String brandName);
// 方式二:对象参数 ------ 对象的属性名要和参数占位符名称一致
//List<Brand> selectByCondition(Brand brand);
// 方式三:Map 集合参数 ------ Map 的 key 要和参数占位符名称一致(当前采用)
List<Brand> selectByCondition(Map map);
2、SQL 映射文件
在 SQL 映射文件 BrandMapper.xml 中 , 定义 List<Brand> selectByCondition(Map map) 接口函数对应的 SQL 脚本 ;
在 BrandMapper.java 接口中 , 定义不同的参数传入方式 , 散装参数、对象参数 、Map 参数 对应的 SQL 映射脚本是相同的 ;
xml
<!--
条件查询(多条件:状态 + 公司名称 + 品牌名称)
id :必须与 BrandMapper 接口方法名 selectByCondition 完全一致(绑定依据)
resultMap :沿用 brandResultMap 封装结果
参数取值 :#{xxx} 中的 xxx 按接口的传参方式自动匹配 ------
① 散装参数 → 匹配 @Param("xxx") 指定的名称
② 对象参数 → 匹配对象的【属性名】xxx
③ Map 参数 → 匹配 Map 的【key】xxx
本条 SQL 对三种传参方式通用 , 切换传参方式无需改动 SQL
like 说明 :% 通配符由调用方拼进参数值中传入(如 "%华为%") ,
#{} 只负责安全占位 , 不会自动拼通配符
-->
<select id="selectByCondition" resultMap="brandResultMap">
select *
from tb_brand
where status = #{status}
and company_name like #{companyName}
and brand_name like #{brandName}
</select>
3、测试类验证
定义 单元测试类 , 测试上述 BrandMapper.java 接口中的 List<Brand> selectByCondition(Map map) 接口函数 ;
java
@Test
public void testSelectByCondition() throws IOException {
// 接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
// 处理参数 : like 模糊查询需拼接 % 通配符
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
// 封装 Map 参数 : key 必须与 SQL 占位符名称一致
Map map = new HashMap();
map.put("status", status);
map.put("companyName", companyName);
map.put("brandName", brandName);
// 中间的加载配置 , 获取 SqlSession , 获取代理对象三步与单条件查询完全相同 , 此处省略
// 执行方法
List<Brand> brands = brandMapper.selectByCondition(map);
System.out.println(brands);
}
测试中隐藏着一个坑 : 静态多条件查询的三个条件必须全部传值 , 如果 Map 中少放一个 key , 该条件按 null 处理 , 例如 status = null 恒不成立 , 整个查询将返回空列表 ; 这一问题正是动态 SQL 要解决的 ;
执行结果 :
- 生成的 SQL 语句 : 是
select * from tb_brand where status = ? and company_name like ? and brand_name like ?; - SQL 语句对应的 3 个参数 : 是
1(Integer), %华为%(String), %华为%(String); - 查询到的 Brand 集合结果 :
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1}];
输出内容 :
dart
[DEBUG] 21:06:03.484 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection
[DEBUG] 21:06:03.725 [main] o.a.i.d.p.PooledDataSource - Created connection 1449263511.
[DEBUG] 21:06:03.725 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@56620197]
[DEBUG] 21:06:03.729 [main] o.e.m.B.selectByCondition - ==> Preparing: select * from tb_brand where status = ? and company_name like ? and brand_name like ?
[DEBUG] 21:06:03.755 [main] o.e.m.B.selectByCondition - ==> Parameters: 1(Integer), %华为%(String), %华为%(String)
[DEBUG] 21:06:03.767 [main] o.e.m.B.selectByCondition - <== Total: 1
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机 , 值得信赖', status=1}]
[DEBUG] 21:06:03.768 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@56620197]
[DEBUG] 21:06:03.768 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@56620197]
[DEBUG] 21:06:03.769 [main] o.a.i.d.p.PooledDataSource - Returned connection 1449263511 to pool.
进程已结束 , 退出代码为 0

五、多条件动态查询
实际业务中用户往往只填部分搜索条件 , 甚至什么都不填 ;
而 SQL 是写死的 , 条件全用 and 硬连接 , 少传一个条件就查不到数据 , 条件缺失时还会拼出 where and ... 这样的语法错误 ;
上一条 selectByCondition 用 and 硬连接三个条件 , 三个条件必须【全部传值】 , 缺任何一个(Map 中少放一个 key)都查不到数据 , 实际业务中用户往往只填部分搜索条件 → 需要「条件按需拼接」 , 用到动态 SQL 标签:
SQL 语句随用户输入的变化而变化 , 这就是动态 SQL ;
MyBatis 为此提供了 if , choose ( when , otherwise ) , trim ( where , set ) , foreach 等标签 ;
1、if 标签 : 条件判断
<if> 的 test 属性书写逻辑表达式 ( OGNL 表达式 ) , 表达式成立才把标签体内的 SQL 拼接进去 ; 判空惯例是 != null 防空指针 , 字符串再加 != '' 防空字符串 ;
2、if 的弊端与恒等式方案
if 标签存在一个经典问题 : 所有条件都以 and 开头时 , 若第一个条件未拼接 , SQL 就变成 where and company_name like ... , 直接语法报错 ; 传统解法是加恒等式 :
xml
<!-- 恒等式方案 : 1 = 1 恒成立 , 保证 where 后面永远有内容 , 每个条件统一以 and 开头 -->
select *
from tb_brand
where 1 = 1
<if test="status != null">
and status = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
恒等式方案能跑通 , 但 1 = 1 与业务无关 , 可读性差 , 属于写死的小技巧 ;
3、where 标签 : 推荐方案
MyBatis 提供 <where> 标签替换 where 关键字 , 它自动做两件事 :
where 标签的两个自动行为 :
- 按需生成 : 内部条件全不成立时不生成 WHERE 子句 , 不会出现空 where 报错 ;
- 剔除符号 : 自动去掉条件开头多余的 and 或 or , 所以 if 内部习惯统一写 and 开头 ;
xml
<!--
1. <if>:条件判断标签
* test 属性:写逻辑表达式(OGNL 表达式) , 为 true 才把标签体内的 SQL 拼进去;
* 判空惯例:!= null 防空指针;字符串再加 != '' 防空字符串
(Map 传参时 test 里的名字就是 Map 的 key)。
2. <where>:替换 where 关键字 , 解决两个尴尬问题:
* 所有 <if> 都不成立时 → 不生成 WHERE 子句(不会出现空的 where 报错);
* 拼上的第一个条件自带 and → 自动【去除开头多余的 and / or】
(所以 <if> 里习惯上都写成 and 开头 , 让 <where> 统一处理)。
======================================================================
-->
<!--
动态条件查询(条件按需拼接)
id :必须与 BrandMapper 接口方法名 selectByConditionDynamic 完全一致(绑定依据)
resultMap :沿用 brandResultMap 封装结果
<if test> :Map 中该 key 存在且非空 , 才拼接对应的 and 条件
<where> :自动处理 where 关键字、去掉开头多余 and、条件全空时不生成 WHERE
【恒等式方案】(传统写法 , 已被 <where> 标签取代 , 注释备查、暂不启用):
不用 <where> 标签时有两个语法问题:条件全不成立 → where 后面没有内容 , SQL 报错;
第一个条件以 and 开头 → where and status = ... 语法错误。
传统解决办法是加恒等式 where 1 = 1(1=1 恒成立 , 保证 where 后永远有内容 ,
后续条件全用 and 连接就不会开头报错):
select *
from tb_brand
where 1 = 1
<if test="status != null">
and status = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != ''">
and brand_name like #{brandName}
</if>
* 缺点:1=1 与业务无关、可读性差 , 属于写死的小技巧 → 推荐用 <where> 标签。
-->
<select id="selectByConditionDynamic" resultMap="brandResultMap">
select *
from tb_brand
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="companyName != null and companyName != ''">
and company_name like #{companyName}
</if>
<if test="brandName != null and brandName != ''">
and brand_name like #{brandName}
</if>
</where>
</select>
测试时只需控制 Map 中放哪些 key : 放了 key 的条件参与拼接 , 注释掉 map.put("brandName", brandName) 这一行 , 品牌名称条件就不会出现在 SQL 中 , 动态效果一目了然 ;
4、Mapper 接口
多条件动态查询 与 多条件查询 的 Mapper 接口定义是相同的 , 其 区别是在 SQL 映射脚本 不同 ;
Mapper 接口函数参考 :
java
/**
* 动态条件查询:条件可选,Map 里放了哪个 key 就按哪个条件查(配合 XML 中 <where>/<if> 标签)
* <p>
* 【与 selectByCondition 的区别】
* 静态版 selectByCondition 的三个条件必须【全部传值】(and 连接,缺一个就查不到数据);
* 实际业务中用户往往只填部分搜索条件,本方法通过动态 SQL 实现「条件按需拼接」:
* 放了 key 的条件拼进 SQL,没放的 key 对应条件直接跳过。
* <p>
* 【XML 端配合的两个标签】(见 BrandMapper.xml 中 selectByConditionDynamic)
* 1. <if test="...">:条件判断标签,test 里写逻辑表达式(OGNL),
* 表达式成立才把该段 SQL 拼进去;判空惯例:!= null,字符串再加 != '';
* 2. <where>:替代 where 关键字 ------ 内部条件全不成立时不生成 WHERE 子句;
* 并自动去掉条件开头多余的 and / or。
* <p>
* 返回值 List<Brand>:符合条件的品牌集合;一个条件都没传时等价于查询全部。
*/
List<Brand> selectByConditionDynamic(Map map);
5、测试方法
定义 单元测试方法 , 调用上面 Mapper 接口函数 ;
java
/**
* 测试:动态条件查询(放哪个 key 就按哪个条件查)
*/
@Test
public void testSelectByConditionDynamic() throws IOException {
//接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
//处理参数:like 模糊查询需拼接 % 通配符
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
//封装 Map 参数:放哪个 key 就拼哪个条件(不放的 key 对应条件不参与查询)
Map map = new HashMap();
map.put("status", status);
//map.put("companyName", companyName);
//map.put("brandName", brandName); // 注释掉:演示「品牌名称」条件不拼接
//1. 加载核心配置文件,获取 SqlSessionFactory
String resource = "mybatis-config-mapper.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取 SqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取 Mapper 接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByConditionDynamic(map);
System.out.println(brands);
//5. 释放资源
sqlSession.close();
}
执行结果 :
- SQL 脚本 : 拼接的 SQL 语句是
select * from tb_brand WHERE status = ?; - SQL 参数 :
1(Integer); - 最终的查询结果 :
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}, Brand{id=2, brandName='小米', companyName='小米科技有限公司', ordered=50, description='小米手机,为发烧而生', status=1}];
dart
[DEBUG] 22:15:55.880 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection
[DEBUG] 22:15:56.101 [main] o.a.i.d.p.PooledDataSource - Created connection 2114444063.
[DEBUG] 22:15:56.102 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7e07db1f]
[DEBUG] 22:15:56.106 [main] o.e.m.B.selectByConditionDynamic - ==> Preparing: select * from tb_brand WHERE status = ?
[DEBUG] 22:15:56.128 [main] o.e.m.B.selectByConditionDynamic - ==> Parameters: 1(Integer)
[DEBUG] 22:15:56.142 [main] o.e.m.B.selectByConditionDynamic - <== Total: 2
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}, Brand{id=2, brandName='小米', companyName='小米科技有限公司', ordered=50, description='小米手机,为发烧而生', status=1}]
[DEBUG] 22:15:56.142 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@7e07db1f]
[DEBUG] 22:15:56.142 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@7e07db1f]
[DEBUG] 22:15:56.143 [main] o.a.i.d.p.PooledDataSource - Returned connection 2114444063 to pool.
进程已结束,退出代码为 0

六、单条件动态查询
另一类场景是 " 从多个条件中选择一个 " : 数据的 搜索类型 是单选 , 选中状态就只按状态查 , 选中企业名称就只按企业名称查 , 什么都不选就查全部 ;
这类似 Java 的 switch 语句 , MyBatis 提供 choose 标签族 ;
1、choose , when , otherwise 标签
三个标签与 Java 语法的对应关系 :
- choose 标签 : 相当于 switch , 内部从上到下判断 , 只命中第一个成立的 when 分支 , 多选一 ;
- when 标签 : 相当于 case , test 属性成立则拼接该段 SQL , 并结束整个 choose ;
- otherwise 标签 : 相当于 default , 所有 when 都不成立时的兜底分支 , 可以省略 ;
2、Mapper 接口函数
单条件 动态查询 接口方法 定义 :
java
/**
* 单条件(单选)动态查询:多个条件【只按一个】查 ------ 从上到下命中第一个非空条件就不再往下看
* <p>
* 【与 selectByConditionDynamic 的区别】
* 动态多条件版:满足 test 的条件【全部拼接】(多个 and 组合查询);
* 本方法:只拼一个条件,类似 Java 的 switch(多选一)。
* <p>
* 【XML 端核心标签】(见 BrandMapper.xml 中 selectByConditionSingleDynamic)
* 1. <choose>:相当于 switch,内部从上到下【只命中一个】分支;
* 2. <when test="...">:相当于 case ------ test 成立则拼接该段 SQL 并结束整个 choose;
* 3. <otherwise>:相当于 default ------ 所有 when 都不成立时的兜底分支(可选)。
* 外层套 <where>:条件以列名开头(无 and),<where> 在有内容时才生成 where 关键字。
* <p>
* 【参数 Brand:对象参数方式】
* #{xxx} 按属性名取值;未赋值的属性为 null,对应 <when> 的 test 不成立、条件被跳过,
* 因此「给哪个属性赋值,就按哪个条件查」。
* <p>
* 返回值 List<Brand>:符合条件的品牌集合;什么都没赋值时查询全部。
*/
List<Brand> selectByConditionSingleDynamic(Brand brand);
3、SQL 映射文件
SQL 映射脚本 配置 , 主要是使用 <choose> , <when> , <otherwise> 标签 根据条件 在多个 SQL 选择语句中筛选出第一个符合条件的语句 ;
xml
<!--
======================================================================
【choose/when/otherwise 标签:单选分支】
======================================================================
* <choose> :相当于 switch ------ 内部从上到下按顺序判断,【只命中第一个】
成立的 <when> 分支,拼完即结束整个 choose(多选一);
与 <if> 的区别:<if> 是「满足就拼、可拼多个」,<choose> 是「只拼一个」。
* <when> :相当于 case ------ test 为 true 则拼接该段 SQL。
* <otherwise>:相当于 default ------ 所有 <when> 都不成立时的兜底分支(可省略)。
======================================================================
-->
<!--
单条件(单选)动态查询 ------ 手动 where + otherwise 兜底 1=1 写法(未启用,仅供对比学习)
手动写 where 关键字时,若所有 when 都不成立,where 后面就空了 → SQL 语法错误,
所以用 <otherwise> 拼 1 = 1 兜底,保证 where 后面永远有内容:
<select id="selectByConditionSingleDynamic" resultMap="brandResultMap">
select *
from tb_brand
where
<choose>(相当于 switch)
<when test="status != null">(相当于 case)
status = #{status}
</when>
<when test="companyName != null and companyName != ''">(相当于 case)
company_name like #{companyName}
</when>
<when test="brandName != null and brandName != ''">(相当于 case)
brand_name like #{brandName}
</when>
<otherwise>
1 = 1
</otherwise>
</choose>
</select>
* 缺点:where 与 1=1 的语法正确性要自己保证,可读性差
→ 推荐下面用 <where> 标签(有内容才生成 where,无需 otherwise 兜底)。
-->
<!--
单条件(单选)动态查询(启用)
id :必须与 BrandMapper 接口方法名 selectByConditionSingleDynamic 完全一致(绑定依据)
resultMap :沿用 brandResultMap 封装结果
参数 :Brand 对象参数,#{xxx} 按属性名取值,未赋值的属性为 null
<choose> :只命中第一个 test 成立的 <when>,实现「多选一」
<where> :有内容才生成 where 关键字(条件以列名开头、无 and,直接正常拼出)
-->
<select id="selectByConditionSingleDynamic" resultMap="brandResultMap">
select *
from tb_brand
<where>
<choose><!--相当于switch-->
<when test="status != null"><!--相当于case-->
status = #{status}
</when>
<when test="companyName != null and companyName != ''"><!--相当于case-->
company_name like #{companyName}
</when>
<when test="brandName != null and brandName != ''"><!--相当于case-->
brand_name like #{brandName}
</when>
</choose>
</where>
</select>
不套 <where> 标签的写法需要手动写 where 关键字 , 此时若所有 when 都不成立 , where 后面就空了 , 必须靠 <otherwise> 拼 1 = 1 兜底 ; 套上 <where> 标签后有内容才生成 where 关键字 , 无需兜底 , 写法更简洁 ;
4、测试类验证
编写 单元测试类 , 调用 Mapper 接口方法 List<Brand> selectByConditionSingleDynamic(Brand brand); ;
java
/**
* 测试:单条件动态查询(多选一,只按第一个非空属性查)
*/
@Test
public void testSelectByConditionSingleDynamic() throws IOException {
//接收参数
int status = 1;
String companyName = "华为";
String brandName = "华为";
//处理参数:like 模糊查询需拼接 % 通配符
companyName = "%" + companyName + "%";
brandName = "%" + brandName + "%";
//封装对象:只给 companyName 赋值,status/brandName 为 null → choose 只命中公司名称分支
Brand brand = new Brand();
//brand.setStatus(status);
brand.setCompanyName(companyName);
//brand.setBrandName(brandName);
//1. 加载核心配置文件,获取 SqlSessionFactory
String resource = "mybatis-config-mapper.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
//2. 获取 SqlSession 对象
SqlSession sqlSession = sqlSessionFactory.openSession();
//3. 获取 Mapper 接口的代理对象
BrandMapper brandMapper = sqlSession.getMapper(BrandMapper.class);
//4. 执行方法
List<Brand> brands = brandMapper.selectByConditionSingleDynamic(brand);
System.out.println(brands);
//5. 释放资源
sqlSession.close();
}
对象参数方式下 , " 给哪个属性赋值就按哪个条件查 " , 未赋值的属性为 null , 对应 when 的 test 不成立被跳过 ; 什么属性都不赋值时等价于查询全部 ;
执行结果 :
- SQL 脚本 :
select * from tb_brand WHERE company_name like ? - SQL 参数 :
%华为%(String) - 查询结果 :
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}]
dart
[DEBUG] 22:34:51.519 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection
[DEBUG] 22:34:51.727 [main] o.a.i.d.p.PooledDataSource - Created connection 371800738.
[DEBUG] 22:34:51.728 [main] o.a.i.t.j.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16293aa2]
[DEBUG] 22:34:51.731 [main] o.e.m.B.selectByConditionSingleDynamic - ==> Preparing: select * from tb_brand WHERE company_name like ?
[DEBUG] 22:34:51.754 [main] o.e.m.B.selectByConditionSingleDynamic - ==> Parameters: %华为%(String)
[DEBUG] 22:34:51.771 [main] o.e.m.B.selectByConditionSingleDynamic - <== Total: 1
[Brand{id=1, brandName='华为', companyName='华为技术有限公司', ordered=100, description='华为手机,值得信赖', status=1}]
[DEBUG] 22:34:51.772 [main] o.a.i.t.j.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.jdbc.JDBC4Connection@16293aa2]
[DEBUG] 22:34:51.772 [main] o.a.i.t.j.JdbcTransaction - Closing JDBC Connection [com.mysql.jdbc.JDBC4Connection@16293aa2]
[DEBUG] 22:34:51.772 [main] o.a.i.d.p.PooledDataSource - Returned connection 371800738 to pool.
进程已结束,退出代码为 0

七、注意事项与避坑
实现四种查询过程中容易踩的坑 , 集中整理如下 :
- 名称必须严格对齐 : @Param 名称 , 对象属性名 , Map 的 key 三者 必须与 SQL 占位符
#{xxx}的 xxx 完全一致 , 写错不会报错 , 只会查到 null 或空结果 ; - Map 少放 key 的隐性 bug : 静态多条件查询少放一个 key , 对应条件按 null 参与比较 , 例如
status = null恒不成立 , 整个查询返回空列表 , 且无任何异常提示 ; - like 的 % 通配符 :
%属于参数值的一部分 ,#{}不会自动拼接 , 需在调用方写成"%" + name + "%", 或在 SQL 中用 concat 函数处理 ; - 接口不支持重载 : 同一个 Mapper 接口中同名方法只能保留一个 , 否则 XML 的 id 冲突 , 运行时报 Invalid bound statement ;
- 字符串判空双条件 :
<if test>中字符串类型要同时写!= null and != '', 只判 null 时空字符串仍会参与拼接 ; - XML 注释的限制 : XML 注释内部不能出现连续两个短横线 , 也不能嵌套注释 , 给注释中的标签做说明时改用普通文字 ;
- 配置文件的选择 : Mapper 代理方式的测试统一加载
mybatis-config-mapper.xml, 它以包扫描方式加载org.example.mapper包下的接口与映射文件 , 二者必须同包同名 ;
总结
本文围绕品牌案例完整实现了四种条件查询 :
- 单条件查询 掌握了
#{}与${}两种参数占位符 以及 parameterType , 特殊字符处理等基础知识 ; - 多条件查询 掌握了散装参数 , 对象参数 , Map 集合三种传参方式及其取舍 ;
- 多条件动态查询 用 if 与 where 标签解决了 " 条件可选 " 的问题 ;
- 单条件动态查询 用 choose , when , otherwise 标签解决了 " 多选一 " 的问题 ;
动态 SQL 的本质是让 SQL 语句 跟随参数变化 按需拼接 , 掌握了这四种姿势 , 就能覆盖绝大多数列表页与详情页的查询开发场景 ;