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

相关推荐
tryCbest2 天前
数据库SQL学习
数据库·sql
cowboy2583 天前
mysql5.7及以下版本查询所有后代值(包括本身)
数据库·sql
GEO行业研究员3 天前
AI是否正在重构个体在健康相关场景中的决策路径——基于系统建模与决策链条结构分析的讨论
人工智能·算法·重构·geo优化·医疗geo·医疗geo优化
努力的lpp3 天前
SQL 报错注入
数据库·sql·web安全·网络安全·sql注入
麦聪聊数据3 天前
统一 Web SQL 平台如何收编企业内部的“野生数据看板”?
数据库·sql·低代码·微服务·架构
山峰哥3 天前
吃透 SQL 优化:告别慢查询,解锁数据库高性能
服务器·数据库·sql·oracle·性能优化·编辑器
前网易架构师-高司机3 天前
带标注的驾驶员安全带识别数据集,识别率99.5%,可识别有无系安全带,支持yolo,coco json,pascal voc xml格式
xml·yolo·数据集·交通·安全带
微学AI3 天前
从云端到指尖:重构 AI 终端生态与实体交互新范式
人工智能·重构·交互
带你看月亮3 天前
第 2 章:重构的原则
重构·模块测试·极限编程
逍遥德3 天前
Maven教程.01- settings.xml 文件<profile>使用详解
xml·java·maven