从 QueryWrapper 到 XML:一次「报表 SQL」的重构实践

文章目录

  • [一、问题背景:一段典型的 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 回到它该待的地方了。

相关推荐
MindCareers6 分钟前
Beta Sprint Day 5-6: Android Development Improvement + UI Fixes
android·c++·git·sql·ui·visual studio·sprint
odoo中国8 分钟前
如何在 Odoo 中从 XML 文件调用函数
xml·odoo·odoo开发·调用函数
nuowenyadelunwen1 小时前
Harvard CS50 week 7 Problem Sets Solutions
数据库·sql·harvard cs50·cs50 week7
驾数者1 小时前
Flink SQL格式集成:JSON、Avro、Protobuf序列化详解
sql·flink·json
Gauss松鼠会1 小时前
【GaussDB】跨用户调用已授权的存储过程,可能会在存储过程内SQL的自定义函数表达式里报错没有权限
数据库·sql·database·gaussdb
趣味科技v2 小时前
全维服务重构汽车消费体验:比亚迪方程豹4S店探店实录
重构·汽车
laocooon52385788611 小时前
mysql,100个题目。
数据库·sql·mysql
笙枫13 小时前
2023-2025年时间序列预测前沿全景报告:从线性反思到十亿级基础模型的范式重构
重构
AI模块工坊13 小时前
【AAAI 2026】即插即用 Spikingformer 重构残差连接,打造高效脉冲 Transformer
深度学习·重构·transformer
cg501715 小时前
力扣数据库——组合两个表
sql·算法·leetcode