订单多条件查询老报错?MyBatis Map传参实战从xml取值到动态sql拼接一步讲透
先别急着重启项目。你大概率是遇到了这几个经典场面之一:
org.apache.ibatis.exceptions.PersistenceException:
### Error querying database. Cause: org.apache.ibatis.reflection.ReflectionException:
There is no getter for property named 'status' in 'class java.util.HashMap'
或者 SQL 跑到数据库里直接被拒:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version
for the right syntax to use near 'AND status = ? AND create_time >= ?' at line 1
甚至更隐蔽的:查出来永远是空,或者全表扫描。
这些报错看着吓人,其实底层逻辑就一句话:MyBatis 拿不到你塞进 Map 的值,或者 XML 拼 SQL 的方式跟数据库的脾气不合。 咱们不绕弯子,直接从“Map 是怎么被 XML 认出来的”讲到“动态 SQL 怎么写才不报错”,顺手把常见坑全部踩平。
Map 传参的本质:XML 在喊名字,不是在读对象
很多人习惯把参数封装成 OrderQueryDTO,但遇到多条件查询时又嫌写类麻烦,直接上 Map<String, Object>。MyBatis 对 Map 的处理非常直白:它在 XML 里看到 #{orderNo},就会去 Map 里调用 get("orderNo")。
这里有两个极易翻车的细节:
- Key 必须完全一致,包括大小写。你 Java 里放的是
"Status",XML 写#{status},必报no getter for property。 - 如果方法签名带了
@Param,Map 会失效。@Param优先级高于 Map,XML 会按注解里的名字去找。所以要么统一用 Map,要么统一用@Param,别混着用。
打个比方:Map 就像一个贴了标签的快递柜。你在柜子里塞了一个包裹,标签写 order_no,但 XML 拿着纸条喊 orderNo,柜员当然找不到。
// ✅ 正确:Java 端塞入的 Key 和 XML 里的 #{ } 完全对齐
Map<String, Object> params = new HashMap<>();
params.put("orderNo", "ORD20240520001");
params.put("status", 1);
params.put("minAmount", 100.00);
params.put("maxAmount", 5000.00);
params.put("startTime", "2024-01-01 00:00:00");
params.put("endTime", "2024-12-31 23:59:59");
params.put("keyword", "手机壳");
#{} 和 ${}:90% 的报错都栽在这两个符号上
这是 MyBatis 传参最核心的分水岭。很多新手看到 #{} 就无脑用,结果遇到排序、模糊查询、动态字段就疯狂报错。
| 符号 | 底层行为 | 安全性 | 典型场景 |
|---|---|---|---|
#{param} |
生成 PreparedStatement 占位符 ?,MyBatis 自动做类型转换并加引号 |
防 SQL 注入 | 普通等值查询、范围比较、LIKE 参数 |
${param} |
直接把字符串拼进 SQL,不做任何转义 | 有注入风险,需自行校验 | 动态排序字段、动态表名、IN 列表拼接 |
为什么 #{keyword} 做模糊查询有时会报错或查不出?
因为 #{keyword} 会被解析成 LIKE ?,而 JDBC 驱动传给数据库时,参数值是纯文本。如果你 Java 端没提前拼 %,数据库收到的就是 LIKE '手机壳',自然查不到带“手机壳”三个字的记录。
正确做法有两种:
<!-- 方式一:Java 端提前拼好 %,推荐 -->
params.put("keyword", "%" + keyword + "%");
<!-- XML -->
AND name LIKE #{keyword}
<!-- 方式二:XML 里用 CONCAT 函数 -->
AND name LIKE CONCAT('%', #{keyword}, '%')
⚠️ 千万别写成 LIKE '%${keyword}%'。${} 是字符串原样拼接,一旦用户输入 1' OR '1'='1,你的订单表就直接裸奔了。
动态 SQL 拼接:让 XML 学会“看情况说话”
多条件查询最怕什么?怕条件为空时还硬拼 AND。比如用户只传了订单号,其他条件都是 null,XML 照写不误,最后 SQL 变成:
SELECT * FROM t_order WHERE AND status = NULL AND amount >= NULL
MySQL 看了直接懵。
MyBatis 的 <where> 和 <if> 就是为了这个场景设计的。它们不是简单的字符串拼接,而是智能过滤:
<select id="queryOrders" resultType="com.example.Order">
SELECT id, order_no, customer_name, status, amount, create_time
FROM t_order
<where>
<!-- 等值查询:非空才拼 -->
<if test="orderNo != null and orderNo != ''">
AND order_no = #{orderNo}
</if>
<!-- 状态查询:注意数字不用加引号,MyBatis 会自动处理 -->
<if test="status != null">
AND status = #{status}
</if>
<!-- 金额范围:两个条件同时成立才生效 -->
<if test="minAmount != null">
AND amount >= #{minAmount}
</if>
<if test="maxAmount != null">
AND amount <= #{maxAmount}
</if>
<!-- 时间范围:建议用 LocalDateTime 或 Date,避免字符串隐式转换 -->
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<if test="endTime != null">
AND create_time <= #{endTime}
</if>
<!-- 模糊查询:用 CONCAT 最稳 -->
<if test="keyword != null and keyword != ''">
AND (customer_name LIKE CONCAT('%', #{keyword}, '%')
OR order_no LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
<!-- 动态排序:必须用 ${},但要拦截非法字段 -->
<if test="orderBy != null and orderBy != ''">
ORDER BY ${orderBy}
</if>
</select>
几个细节值得单独拎出来说:
<where>会自动去掉第一个AND/OR,如果所有<if>都不满足,它连WHERE关键字都不会输出。这比手动写<trim prefix="WHERE" prefixOverrides="AND |OR ">省心太多。- XML 里不能直接写
<和>,会被当成标签解析。必须用实体字符:<、>、&。很多人复制粘贴不等号直接报错The content of elements must consist of well-formed character data...,其实就是这个原因。 - 空字符串和 null 要分开判断。Java 里
""和null是两码事。订单号可能传了空字符串,金额不可能传空字符串。用!= null and != ''能拦住大部分脏数据。
完整链路:从 Controller 到 XML 一次跑通
光看 XML 不够,咱们把整条调用链串起来,你照着改就能用。
Mapper 接口
public interface OrderMapper {
/**
* 多条件查询订单
* 注意:参数用 Map 时,不需要 @Param,MyBatis 默认按 Map 处理
*/
List<Order> queryOrdersByCondition(Map<String, Object> params);
}
Service 层组装参数
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMapper orderMapper;
@Override
public PageResult<Order> queryOrders(OrderQueryRequest request) {
Map<String, Object> params = new HashMap<>();
// 订单号精确匹配
if (StringUtils.hasText(request.getOrderNo())) {
params.put("orderNo", request.getOrderNo());
}
// 状态
if (request.getStatus() != null) {
params.put("status", request.getStatus());
}
// 金额范围,防止传负数或越界
if (request.getMinAmount() != null && request.getMinAmount().compareTo(BigDecimal.ZERO) >= 0) {
params.put("minAmount", request.getMinAmount());
}
if (request.getMaxAmount() != null) {
params.put("maxAmount", request.getMaxAmount());
}
// 时间范围,推荐用 LocalDateTime,JDBC 驱动会自动映射
if (request.getStartTime() != null) {
params.put("startTime", request.getStartTime());
}
if (request.getEndTime() != null) {
params.put("endTime", request.getEndTime());
}
// 模糊搜索关键词
if (StringUtils.hasText(request.getKeyword())) {
params.put("keyword", request.getKeyword());
}
// 排序字段:前端传什么,这里必须做白名单校验!
if ("amount_asc".equals(request.getOrderBy())) {
params.put("orderBy", "amount ASC");
} else if ("amount_desc".equals(request.getOrderBy())) {
params.put("orderBy", "amount DESC");
} else if ("create_time_desc".equals(request.getOrderBy())) {
params.put("orderBy", "create_time DESC");
}
// 其他一律忽略,绝不把用户输入直接塞进 ${}
return new PageResult<>(orderMapper.queryOrdersByCondition(params));
}
}
为什么排序字段一定要白名单?
因为 ORDER BY ${orderBy} 用的是字符串拼接。如果前端传 orderBy=amount; DROP TABLE t_order--,你的数据库真的会执行。白名单是最简单也最安全的防线。
常见报错对照表:对着症状直接改
| 报错关键词 | 真实原因 | 解法 |
|---|---|---|
There is no getter for property named 'xxx' |
XML 里的 #{xxx} 和 Map 的 Key 不一致,或方法没传 Map |
检查大小写;确认方法签名是 Map 且没写 @Param |
Invalid bound statement (not found) |
XML 的 namespace 和 Mapper 接口路径不一致 | 确保 mapperNamespace="com.xxx.OrderMapper" |
You have an error in your SQL syntax... near 'AND' |
<if> 全为空,但手写 WHERE AND ... 没包 <where> |
用 <where> 包裹条件,或加 <trim> |
Can not resolve type alias 'java.util.Map' |
XML 里 resultType 写成了 Map,但实际返回的是实体 | 返回集合用 resultType="com.xxx.Order",别写 Map |
| 查出来数据永远为空 | #{} 没拼 %,或日期字符串格式和数据库不匹配 |
用 CONCAT('%',#{k},'%');日期传 LocalDateTime 或 yyyy-MM-dd HH:mm:ss |
Parameter 'x' not found |
方法参数用了 @Param("a"),但 XML 读的是 #{x} |
@Param 后 XML 必须用注解里的名字 |
进阶:如果条件特别多,XML 会不会乱?
当查询条件超过 10 个,<if> 堆在一起确实容易眼花。这时候可以用 <trim> 做精细控制,或者把条件拆成多个 <choose> 分支。
举个例子,状态不同,查询策略完全不同:
<choose>
<!-- 全部订单 -->
<when test="status == null or status == 0">
<!-- 不拼 status 条件 -->
</when>
<!-- 已支付 -->
<when test="status == 1">
AND pay_status = 1
</when>
<!-- 待发货 -->
<when test="status == 2">
AND pay_status = 1 AND ship_status = 0
</when>
<!-- 兜底 -->
<otherwise>
AND 1=0
</otherwise>
</choose>
这种写法比层层嵌套 <if> 清晰得多,数据库执行计划也更稳定。
调试小技巧:让 MyBatis 把真 SQL 打印出来
很多时候报错是因为你以为拼的是 A,实际拼的是 B。开启 SQL 日志,一眼看穿:
# application.yml
mybatis:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
跑一次查询,控制台会原样输出:
==> Preparing: SELECT id, order_no, customer_name, status, amount, create_time FROM t_order WHERE order_no = ? AND status = ? AND amount >= ? AND customer_name LIKE CONCAT('%', ?, '%')
==> Parameters: ORD20240520001(String), 1(Integer), 100.00(Double), 手机壳(String)
<== Total: 3
看到 Preparing 就知道占位符结构,看到 Parameters 就知道实际传入的值。如果这里出现 NULL、空字符串、或者类型不对,直接回头检查 Service 层的 params.put()。
多条件查询报错,本质上就是参数名没对齐、符号选错、动态拼接没兜底这三件事。把 Map 的 Key 和 XML 的 #{} 盯死,该用 #{} 绝不用 ${},排序和表名必须白名单,再配上 <where> 和 <if> 的智能过滤,这类问题基本不会再来找你。
下次再遇到类似报错,先开日志看 Preparing 那行 SQL,90% 的问题在参数列表里就能定位。代码照着结构搭,按自己的业务字段替换即可。有具体报错信息也可以贴出来,咱们对着日志一行行拆。