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

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

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


1. 需求建模

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

  1. 维度(dimensions)

    按哪些字段分组,如 shop_idsale_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 引擎的核心实现方案。

相关推荐
千层冷面1 小时前
Flask ORM 查询详解:Model.query vs db.session.query vs db.session.execute
数据库·python·django·flask
Navicat中国2 小时前
Navicat 询问 AI | 如何转换 SQL 为另一种数据库类型
数据库·人工智能·sql·数据库开发·navicat
Runing_WoNiu2 小时前
Redis核心架构
数据库·redis·架构
北十南2 小时前
SODA自然美颜相机(甜盐相机国际版) v9.3.0
android·windows·数码相机
电商API_180079052472 小时前
大规模调用淘宝商品详情 API 的分布式请求调度实践
服务器·数据库·分布式·爬虫
weixin_446260853 小时前
windows下hashcat使用gpu破解execl打开密码
windows
Menior_3 小时前
【补充】数据库中有关系统编码和校验规则的简述
数据库·mysql·oracle
晴子呀4 小时前
分库分表和sql的进阶用法总结
数据库·sql