用 FreeMarker 动态构造 SQL 实现数据透视分析

在 ERP、BI 等系统中,数据透视分析(Pivot Analysis)是非常常见的需求:用户希望按任意维度(如门店、时间、商品分类等)进行分组统计,同时选择不同的指标(如 GMV、订单数、客单价等)进行聚合计算。

这种需求如果直接写固定 SQL 会非常死板,而通过 FreeMarker 模板 动态构造 SQL,我们可以在保证安全的前提下,让维度、指标、过滤条件、排序、分页等全部可配置化,从而实现灵活的数据分析能力。


1. 需求建模

一个典型的数据透视分析需要定义三部分信息:

  1. 维度(dimensions)

    按哪些字段分组,如 shop_id、sale_date 等。

  2. 指标(measures)

    对哪些字段做聚合计算,如 SUM(amount)、COUNT(order_id)。

    例如你提供的结构:

    复制代码
    "measures": [
      { "func": "sum",   "field": "amount",   "alias": "gmv" },
      { "func": "count", "field": "order_id", "alias": "orders" }
    ]
  3. 过滤条件(filters)

    如时间区间、门店、商品分类等限制。


2. 安全策略

动态 SQL 最大的风险是 SQL 注入,所以必须做到:

  • 列名白名单 :

    允许的字段映射表,如:

    复制代码
    Map<String, String> columnMap = Map.of(
        "amount",   "t.amount",
        "order_id", "t.order_id",
        "shop_id",  "t.shop_id"
    );
  • 聚合函数白名单:

    复制代码
    Set<String> aggs = Set.of("SUM", "COUNT", "AVG", "MIN", "MAX");
  • 值参数绑定 :

    所有值通过命名参数 :param 传入,不直接拼到 SQL 字符串中。


3. FreeMarker SQL 模板

下面是一个支持维度、指标、过滤、排序、分页的 FreeMarker 模板 pivot.ftl:

复制代码
SELECT
  <#-- 维度 -->
  <#if dimensions?has_content>
    <#list dimensions as d>
      ${d.sql} AS ${d.alias}<#if d_has_next>,</#if>
    </#list>
    <#if measures?has_content>,</#if>
  </#if>

  <#-- 指标 -->
  <#list measures as m>
    ${m.sql} AS ${m.alias}<#if m_has_next>,</#if>
  </#list>

FROM ${table} t

<#if whereClauses?has_content>
WHERE
  <#list whereClauses as w>
    (${w})<#if w_has_next> AND </#if>
  </#list>
</#if>

<#if dimensions?has_content>
GROUP BY
  <#list dimensions as d>
    ${d.sql}<#if d_has_next>,</#if>
  </#list>
</#if>

<#if orderBy?has_content>
ORDER BY
  <#list orderBy as o>
    ${o.sql} ${o.dir}<#if o_has_next>,</#if>
  </#list>
</#if>

<#if limit??>
LIMIT :p_limit
  <#if offset??>
  OFFSET :p_offset
  </#if>
</#if>

4. Java 构造 SQL

在 Java 端,根据前端传入的 {func, field, alias} 格式,映射成模板可用的 SQL 片段。

复制代码
List<Map<String, Object>> measureList = measures.stream().map(m -> {
    String agg = m.getFunc().toUpperCase();
    if (!aggs.contains(agg)) {
        throw new IllegalArgumentException("非法聚合函数: " + agg);
    }
    String col = Optional.ofNullable(columnMap.get(m.getField()))
            .orElseThrow(() -> new IllegalArgumentException("非法列: " + m.getField()));
    String sqlExpr = agg + "(" + col + ")";
    return Map.of(
        "name", m.getField(),
        "alias", m.getAlias(),
        "sql", sqlExpr
    );
}).toList();

这样,如果用户传入:

复制代码
"measures": [
  { "func": "sum",   "field": "amount",   "alias": "gmv" },
  { "func": "count", "field": "order_id", "alias": "orders" }
]

模板渲染后就会生成:

复制代码
SELECT
  SUM(t.amount) AS gmv,
  COUNT(t.order_id) AS orders
FROM order_table t

5. 加入维度

如果加上维度:

复制代码
"dimensions": [
  { "field": "shop_id", "alias": "shop" }
]

生成的 SQL 会是:

复制代码
SELECT
  t.shop_id AS shop,
  SUM(t.amount) AS gmv,
  COUNT(t.order_id) AS orders
FROM order_table t
GROUP BY t.shop_id

6. 好处

  • 灵活性高:维度、指标、过滤条件全可配置。

  • 安全:列、函数、表等全部走白名单;值参数绑定,防止注入。

  • 可扩展:可以轻松加入条件聚合实现行转列(透视列)。

  • 通用性:同一套模板可支持不同业务场景(销售、库存、财务等)。


7. 总结

通过 FreeMarker 模板 + 白名单映射 + 参数绑定,我们可以优雅地实现一个安全、灵活的数据透视分析引擎。

核心要点:

  1. 模板只负责结构,不直接拼用户输入。

  2. Java 端负责校验、映射、生成 SQL 片段。

  3. 所有值用参数绑定,杜绝 SQL 注入。

这种方式不仅可以满足 ERP 复杂报表的需求,也能作为通用 BI 引擎的核心实现方案。

相关推荐
CoderYanger5 小时前
Java EE 进阶:8.MyBatis 操作数据库(入门)(第二弹)
java·数据库·spring boot·spring·面试·java-ee·mybatis
wafrdggh8 小时前
2026电商快递批量查询怎么用?固乔助手模板设置评测
大数据·数据库·小程序
一个天蝎座 白勺 程序猿8 小时前
KingbaseES数据库归档重做日志文件管理:从配置到运维的实战指南
运维·数据库·oracle·kingbasees·金仓数据库
龙腾AI白云11 小时前
仿真到现实(Sim-to-Real)迁移:跨越仿真与现实鸿沟
数据库·机器学习·plotly·知识图谱·tornado
freejackman14 小时前
【实战二】字段级血缘追踪:拆解 AST 迷局,精准穿透函数链与子查询
大数据·数据库·sql
艺杯羹14 小时前
Windows原生软件锁设计:从暴力杀进程丢数据到零轮询事件驱动架构
windows·系统架构·开源·开源软件·内存管理·开源协议
.痞老板.15 小时前
mysql初阶--增删改查操作
数据库·mysql
lambo mercy20 小时前
AI智能租房助手
数据库
浮生若城1 天前
MySQL:库与表的操作
数据库·mysql
薛定谔的悦1 天前
光储充一体机从主控到执行单元的双层架构
数据库·mongodb·架构