在 ERP、BI 等系统中,数据透视分析(Pivot Analysis)是非常常见的需求:用户希望按任意维度(如门店、时间、商品分类等)进行分组统计,同时选择不同的指标(如 GMV、订单数、客单价等)进行聚合计算。
这种需求如果直接写固定 SQL 会非常死板,而通过 FreeMarker 模板 动态构造 SQL,我们可以在保证安全的前提下,让维度、指标、过滤条件、排序、分页等全部可配置化,从而实现灵活的数据分析能力。
1. 需求建模
一个典型的数据透视分析需要定义三部分信息:
-
维度(dimensions)
按哪些字段分组,如
shop_id
、sale_date
等。 -
指标(measures)
对哪些字段做聚合计算,如
SUM(amount)
、COUNT(order_id)
。例如你提供的结构:
"measures": [ { "func": "sum", "field": "amount", "alias": "gmv" }, { "func": "count", "field": "order_id", "alias": "orders" } ]
-
过滤条件(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 模板 + 白名单映射 + 参数绑定,我们可以优雅地实现一个安全、灵活的数据透视分析引擎。
核心要点:
-
模板只负责结构,不直接拼用户输入。
-
Java 端负责校验、映射、生成 SQL 片段。
-
所有值用参数绑定,杜绝 SQL 注入。
这种方式不仅可以满足 ERP 复杂报表的需求,也能作为通用 BI 引擎的核心实现方案。