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

相关推荐
风娜4 分钟前
用 Claude Code 重构遗留系统:从混乱到有序的实战指南
重构
其实防守也摸鱼7 分钟前
CTF Web 6 题全解 Writeup
linux·sql·学习·算法·安全·ctf·xss
小小龙学IT24 分钟前
PostgreSQL 服务端机制与 SQL 实战深度解析:从 MVCC 到查询优化器
数据库·sql·postgresql
l1t31 分钟前
DeepSeek总结的DuckDB 从 JSON 字符串到类型化列
数据库·sql·json
StarRocks_labs1 小时前
StarRocks 接入 Paimon C++:让湖上查询进入原生执行链路
starrocks·c++·sql·jni·paimon·auto·湖上查询
小师兄吃牛肉1 小时前
SQL 编辑器异常排查:基于 KStudio 的一次实战记录
java·sql·编辑器
泡海椒1 小时前
Spring 项目中如何组织 JQuick-Excel 的 XML 与 Bean
xml·spring·excel
骑着蜗牛撵大象3272 小时前
SQL 编辑器不是高级记事本:KStudio 与 KSQL Developer 里的建表与增删改查手法
数据库·sql·mysql·编辑器·crud·执行计划
CaptainT2 小时前
WHERE 到底筛掉了哪些行
sql·mysql
程序 代码狂人3 小时前
SQL COUNT 统计 NULL 与空字符串 ‘‘ 差异(Oracle & Impala)
数据库·sql·oracle