文章目录
- [一、问题背景:一段典型的 QueryWrapper 报表代码](#一、问题背景:一段典型的 QueryWrapper 报表代码)
- [二、为什么「报表统计」不适合用 QueryWrapper?](#二、为什么「报表统计」不适合用 QueryWrapper?)
- 三、重构目标
- 四、重构后的整体结构
- [五、Mapper 接口(只定义行为)](#五、Mapper 接口(只定义行为))
- [六、核心:XML 动态 SQL(重点)](#六、核心:XML 动态 SQL(重点))
- [七、Service 层:字段白名单校验](#七、Service 层:字段白名单校验)
- [八、结果 DTO(拒绝 Map)](#八、结果 DTO(拒绝 Map))
- 九、重构前后对比总结
- [十、什么时候该回归 SQL?](#十、什么时候该回归 SQL?)
在使用 MyBatis-Plus 的过程中, QueryWrapper / LambdaQueryWrapper 极大地提升了 CRUD 开发效率。但当业务进入 报表统计、复杂聚合、多维分析 阶段时,继续使用 Wrapper 往往会带来:
- SQL 可读性差
- 维护成本高
- 动态字段安全隐患
- 逻辑分散、不利于 DBA / BI 协作
本文将通过一次真实的重构案例,讲清楚:
为什么报表场景不适合 Wrapper?
以及如何将其重构为"企业级可维护"的 XML SQL。
一、问题背景:一段典型的 QueryWrapper 报表代码
最初的报表逻辑使用 QueryWrapper 拼接:
- 动态统计字段(人数 / 订单数 / 金额)
- 动态分组字段(日期 / 省份 / 品牌)
- 多条件过滤
代码大致如下(简化版):
java
private QueryWrapper<VOrderInfo> getMyReportWrapper(
QueryWrapper<VOrderInfo> qw,
VOrderInfoJSONObject param) {
String selectStr;
if ("order_amount".equals(param.getCountKeyword())) {
selectStr = "sum(order_amount) as count";
} else {
selectStr = "count(distinct " + param.getCountKeyword() + ") as count";
}
if ("create_date".equals(param.getGroupKeyword())) {
qw.select(
"DATE_FORMAT(create_date,'%Y-%m-%d') as groupTag",
selectStr
).groupBy("DATE_FORMAT(create_date,'%Y-%m-%d')");
} else {
qw.select(param.getGroupKeyword() + " as groupTag", selectStr)
.groupBy(param.getGroupKeyword());
}
qw.eq(StringUtils.hasText(param.getSkuName()), "sku_name", param.getSkuName());
qw.eq(StringUtils.hasText(param.getProvinceName()), "province_name", param.getProvinceName());
qw.eq(StringUtils.hasText(param.getTmName()), "tm_name", param.getTmName());
return qw;
}
初看似乎没问题,但实际存在几个痛点:
- SQL 被拆散在 Java 逻辑中,不直观
- 聚合 + group by 变成字符串拼接
- 动态字段存在 SQL 注入风险
- 后期新增统计维度需要频繁改 Java 代码
二、为什么「报表统计」不适合用 QueryWrapper?
QueryWrapper 的设计初衷
QueryWrapper 非常适合:
- 单表 CRUD
- 列表查询
- 条件筛选(eq / like / between)
但它并不是为复杂 SQL 设计的。
在报表场景下的问题
| 问题 | 说明 |
|---|---|
| 可读性差 | 看 Java 才能脑补 SQL |
| 表达能力弱 | DATE_FORMAT / CASE / HAVING 不自然 |
| 动态字段危险 | 本质仍是字符串 |
| 不利于协作 | DBA、BI 无法直接维护 |
结论:
报表统计 ≠ CRUD
报表统计 = SQL 的主战场
三、重构目标
这次重构的目标很明确:
- SQL 集中、直观、可维护
- Java 负责 参数校验
- SQL 支持 动态分组 + 动态指标
- 从一开始就 防 SQL 注入
最终选择方案:
MyBatis XML + 动态 SQL
四、重构后的整体结构
service
└── VOrderInfoService
mapper
├── VOrderInfoMapper.java
dto
├── VOrderInfoJSONObject.java // 入参
└── ReportResult.java // 出参
resources
└── mapper
└── VOrderInfoMapper.xml
五、Mapper 接口(只定义行为)
java
@Mapper
public interface VOrderInfoMapper {
List<ReportResult> selectMyReport(@Param("param") VOrderInfoJSONObject param);
}
Mapper 接口不写 SQL,是企业项目中最清爽的状态。
六、核心:XML 动态 SQL(重点)
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.donglin.report.mapper.VOrderInfoMapper">
<select id="selectMyReport" resultType="com.donglin.report.domain.ReportResult">
SELECT
<!-- 分组字段 -->
<choose>
<when test="param.groupKeyword == 'create_date'">
DATE_FORMAT(create_date,'%Y-%m-%d') AS groupTag
</when>
<otherwise>
${param.groupKeyword} AS groupTag
</otherwise>
</choose>
,
<!-- 统计字段 -->
<choose>
<when test="param.countKeyword == 'order_amount'">
SUM(order_amount)
</when>
<otherwise>
COUNT(DISTINCT ${param.countKeyword})
</otherwise>
</choose>
AS count
FROM v_order_info
<where>
<if test="param.skuName != null and param.skuName != ''">
AND sku_name = #{param.skuName}
</if>
<if test="param.provinceName != null and param.provinceName != ''">
AND province_name = #{param.provinceName}
</if>
<if test="param.tmName != null and param.tmName != ''">
AND tm_name = #{param.tmName}
</if>
</where>
GROUP BY
<choose>
<when test="param.groupKeyword == 'create_date'">
DATE_FORMAT(create_date,'%Y-%m-%d')
</when>
<otherwise>
${param.groupKeyword}
</otherwise>
</choose>
</select>
</mapper>
XML 的优势一目了然:
- SQL 结构完整
- 分组、聚合逻辑清晰
- 扩展维度只需改 SQL
七、Service 层:字段白名单校验
由于使用了 ${} 动态字段,必须做白名单校验。
java
@Service
public class VOrderInfoService {
private static final Set<String> GROUP_WHITE_LIST = Set.of(
"create_date", "province_name", "tm_name"
);
private static final Set<String> COUNT_WHITE_LIST = Set.of(
"order_id", "user_id", "order_amount"
);
@Autowired
private VOrderInfoMapper mapper;
public List<ReportResult> myReport(VOrderInfoJSONObject param) {
checkParam(param);
return mapper.selectMyReport(param);
}
private void checkParam(VOrderInfoJSONObject param) {
if (!GROUP_WHITE_LIST.contains(param.getGroupKeyword())) {
throw new IllegalArgumentException("非法分组字段");
}
if (!COUNT_WHITE_LIST.contains(param.getCountKeyword())) {
throw new IllegalArgumentException("非法统计字段");
}
}
}
安全不是 SQL 的问题,是使用方式的问题。
八、结果 DTO(拒绝 Map)
java
@Data
public class ReportResult {
private String groupTag;
private BigDecimal count;
}
- 明确字段含义
- 易扩展
- 类型安全
九、重构前后对比总结
| 维度 | QueryWrapper | XML SQL |
|---|---|---|
| 可读性 | 一般 | ⭐⭐⭐⭐⭐ |
| 表达能力 | 受限 | 无限 |
| 安全性 | 易忽略 | 可控 |
| 扩展性 | 改 Java | 改 SQL |
| 协作友好 | 否 | 是 |
十、什么时候该回归 SQL?
我的经验总结是:
CRUD → LambdaQueryWrapper
报表 / 统计 / BI → XML SQL
不是 Wrapper 不好,而是 工具要用在合适的地方。
当你发现自己在 Java 里"拼 SQL 字符串"时,
那往往是一个信号:
👉 该让 SQL 回到它该待的地方了。