从 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 回到它该待的地方了。

相关推荐
KKKlucifer4 小时前
国内数据安全服务的产品智能化转型:大模型赋能下的技术能力重构分析
大数据·数据库·重构
nm_zfyjt7 小时前
信息流广告投放优化实践:线索成本评估模型与转化链路重构(家装建材案例)
算法·重构·内蒙古朋友圈广告服务商·内蒙古朋友圈获客
WiKiLeaks_successor8 小时前
重构Time库的抽象设计
python·重构
herinspace12 小时前
管家婆实用贴-SQL 2008如何修改成混合模式
服务器·数据库·sql·管家婆软件·财务软件·合肥管家婆软件
大模型最新论文速读14 小时前
text-to-sql:从学术玩具到生产落地
数据库·sql
Decay__15 小时前
15_MySQL 数据同步与迁移方案 —— 复制 / DataX / Canal / 异构同步(对标 Oracle GoldenGate)
运维·数据库·sql·mysql·架构
zjx238816 小时前
oracle11g转Gbase8s库SQL常见问题修改
数据库·sql
杨云龙UP17 小时前
Oracle 同义词(SYNONYM)详解及 Toad for Oracle SQL 智能补全配置实战
运维·数据库·sql·oracle·同义词·toad·toad for oracle
Decay__17 小时前
17_MySQL 监控体系构建 —— mysqld_exporter + Prometheus + Grafana 全栈实战
运维·数据库·sql·mysql·架构
小瓜66918 小时前
Claude Code替代软件哪个好?TRAE vs Claude Code深度对比:从中文开发、复杂重构到团队成本
重构