基于 LiteFlow 和业务规则树的动态 SQL 生成方案

138 阅读6分钟

文档摘要

本文档详细阐述了如何利用 LiteFlow 规则引擎,将一棵由业务指标和维度组成的规则树,动态解析并拼接成可执行的 SQL 查询语句。 该方案解决了复杂、多变业务规则下数据查询的灵活性与可维护性问题。

项目说明
核心框架LiteFlow
关键技术规则引擎、流程编排、SQL 生成、树结构解析
目标读者后端开发工程师、架构师、数据工程师

1. 应用场景与核心问题

1.1 场景描述

在复杂的业务系统(如风控、营销、高级报表)中,查询条件通常不是固定的。它们源自一系列可灵活配置的业务规则,例如:

规则(城市 ∈ ["北京", "上海"] AND 年龄 > 30) OR (最近下单天数 < 7 AND 订单金额 > 1000)

这种规则可以被表示为一棵业务规则树,其中非叶子节点是逻辑运算符(AND/OR),叶子节点是具体的维度条件。

1.2 核心问题

  • 硬编码困境:传统的 if-elseMyBatis 动态标签难以维护这种嵌套深、变化频繁的逻辑。
  • 可维护性差:业务逻辑与代码耦合深,规则变更需重新开发、测试、上线。
  • 灵活性不足:无法支持业务人员通过界面拖拽生成新的查询规则。

1.3 解决方案

使用 LiteFlow 规则引擎,将规则树的解析和 SQL 片段的组装过程抽象为一个个可复用的组件,通过** EL 规则**进行流程编排,实现高度灵活和可维护的动态 SQL 生成。


2. 系统架构与设计理念

2.1 核心架构

      +-------------------+     +-----------------------+
      |   业务规则树        |     |     LiteFlow Context     |
      |  (JSON/DB)        |     | +-------------------+ |
      | - Root: OR        |     | | globalSqlBuilder  | |--> 最终SQL
      |   - AND           | --> | |   (StringBuffer)  | |
      |     - (维度条件A)   |     | | treeRootNode     | |
      |     - (维度条件B)   |     | +-------------------+ |
      |   - AND           |     +-----------------------+
      |     - (指标条件C)   |                 |
      +-------------------+                 v
                                     +--------------+
                                     | LiteFlow Chain|
                                     | (EL规则编排)   |
                                     +--------------+

2.2 设计理念:组件化与编排

  • 组件化:将每一种维度/指标条件的拼接逻辑封装成一个独立的 LiteFlow 节点(Node)。例如:CityConditionCmp, AgeConditionCmp
  • 编排:规则树的遍历逻辑由 LiteFlow 的 EL 规则(THEN, SWITCH, IF)来驱动。执行流程业务逻辑彻底解耦。

3. 技术实现详解

3.1 数据结构定义 (规则树)

首先,需定义规则树的 JSON 结构。

{
  "logicalOperator": "OR", // 非叶子节点:逻辑运算符 "AND" / "OR"
  "children": [            // 子节点列表
    {
      "logicalOperator": "AND",
      "children": [
        {
          "type": "dimension", // 叶子节点:维度条件
          "field": "city",
          "operator": "in",
          "value": ["北京", "上海"]
        },
        {
          "type": "metric", // 叶子节点:指标条件
          "field": "age",
          "operator": ">",
          "value": 30
        }
      ]
    },
    {
      "logicalOperator": "AND",
      "children": [
        {
          "type": "metric",
          "field": "days_since_last_order",
          "operator": "<",
          "value": 7
        },
        {
          "type": "metric",
          "field": "order_amount",
          "operator": ">",
          "value": 1000
        }
      ]
    }
  ]
}

3.2 LiteFlow 组件设计

3.2.1 公共上下文 (Context)

@Data
public class SqlContext implements Context {
    // 用于拼接最终SQL的容器
    private StringBuffer sqlBuilder = new StringBuffer();
    // 参数Map,用于PreparedStatement防SQL注入
    private Map<String, Object> paramMap = new HashMap<>();
    private int paramIndex = 1; // 参数索引计数器,用于生成 :param1, :param2

    // 当前正在处理的规则树节点
    private TreeNode currentNode;

    // 添加条件的方法
    public void addCondition(String conditionFragment, Object value) {
        if (sqlBuilder.length() > 0) {
            // 如何拼接(AND/OR)由父节点控制,这里不处理
        }
        String paramName = "param" + (paramIndex++);
        sqlBuilder.append(" ").append(conditionFragment.replace("?", ":" + paramName));
        paramMap.put(paramName, value);
    }
}

3.2.2 节点组件实现 (NodeComponent)

a. 逻辑节点组件 (非叶子节点)

// 抽象逻辑节点
@Component
public abstract class LogicalNodeCmp extends NodeComponent {
    @Override
    public void process() {
        SqlContext context = this.getContext();
        TreeNode currentNode = context.getCurrentNode();

        // 1. 处理当前逻辑操作符 (例如,在进入AND节点时添加 '(' )
        context.getSqlBuilder().append(" ").append(getLogicalOperator()).append(" (");

        // 2. 遍历子节点,并执行它们
        for (TreeNode child : currentNode.getChildren()) {
            context.setCurrentNode(child); // 设置当前要处理的子节点
            // 根据子节点的类型,动态决定下一个要执行的组件
            this.executeChild(context, child);
        }

        // 3. 结束当前逻辑组
        context.getSqlBuilder().append(" )");
    }

    protected void executeChild(SqlContext context, TreeNode child) {
        if ("dimension".equals(child.getType()) || "metric".equals(child.getType())) {
            // 如果是叶子节点,根据 field 和 operator 路由到具体条件组件
            // 例如:city -> CityConditionCmp, age -> AgeConditionCmp
            String nodeId = "cond_" + child.getField() + "_" + child.getOperator(); 
            this.execute(context, nodeId);
        } else {
            // 如果是非叶子节点,根据逻辑操作符路由到逻辑组件
            String nodeId = "logical_" + child.getLogicalOperator().toLowerCase();
            this.execute(context, nodeId);
        }
    }

    protected abstract String getLogicalOperator();
}

// 具体实现
@Component("logical_and")
public class AndNodeCmp extends LogicalNodeCmp {
    @Override protected String getLogicalOperator() { return "AND"; }
}

@Component("logical_or")
public class OrNodeCmp extends LogicalNodeCmp {
    @Override protected String getLogicalOperator() { return "OR"; }
}

b. 条件节点组件 (叶子节点)

// 城市条件组件
@Component("cond_city_in")
public class CityInConditionCmp extends NodeComponent {
    @Override
    public void process() {
        SqlContext context = this.getContext();
        TreeNode node = context.getCurrentNode();

        @SuppressWarnings("unchecked")
        List<String> cities = (List<String>) node.getValue();
        // 生成SQL片段,例如:city IN (:param1, :param2)
        String placeholders = String.join(", ", Collections.nCopies(cities.size(), "?"));
        context.addCondition("city IN (" + placeholders + ")", cities);
    }
}

// 年龄条件组件
@Component("cond_age_>")
public class AgeGtConditionCmp extends NodeComponent {
    @Override
    public void process() {
        SqlContext context = this.getContext();
        TreeNode node = context.getCurrentNode();
        context.addCondition("age > ?", node.getValue());
    }
}

3.3 规则编排 (EL规则)

这是最关键的部分,但它的逻辑可以变得非常简单,因为复杂的路由逻辑已被封装在 LogicalNodeCmp 中。

<!-- 主要规则链:只需启动逻辑节点的执行 -->
<chain name="buildDynamicSqlChain">
    SWITCH(decisionCmp).to(logical_and, logical_or);
</chain>

<!-- 一个决策组件,决定从根节点开始执行AND还是OR -->
@Component
public class DecisionCmp extends NodeComponent {
    @Override
    public String process() {
        SqlContext context = this.getContext();
        TreeNode rootNode = context.getCurrentNode(); // 假设根节点已被设置
        return "logical_" + rootNode.getLogicalOperator().toLowerCase();
    }
}

3.4 服务层调用

@Service
public class DynamicQueryService {

    @Resource
    private LiteFlowComponent liteFlowComponent;

    public String buildDynamicSql(TreeNode ruleTreeRoot) {
        // 1. 创建上下文,并设置规则树根节点
        SqlContext context = new SqlContext();
        context.setCurrentNode(ruleTreeRoot);
        context.getSqlBuilder().append("SELECT * FROM user_table WHERE 1=1"); // 基础SQL

        // 2. 执行LiteFlow规则链
        LiteflowResponse response = liteFlowComponent.execute2Resp(
            "buildDynamicSqlChain", null, context
        );

        // 3. 获取最终SQL和参数
        if (response.isSuccess()) {
            String finalSql = context.getSqlBuilder().toString();
            Map<String, Object> params = context.getParamMap();
            // 使用JdbcTemplate/MyBatis等执行 finalSql 和 params
            return finalSql;
        } else {
            throw new RuntimeException("SQL构建失败: " + response.getMessage());
        }
    }
}

4. 技术难点与解决方案

难点描述解决方案
树形结构递归如何优雅地遍历不确定深度的规则树。利用 LiteFlow 组件自嵌套执行this.execute())。逻辑节点处理自身逻辑后,动态决定并执行子节点,天然支持递归。
组件路由如何根据动态的 fieldoperator 路由到正确的组件。设计统一的组件命名规则(如 cond_{field}_{operator}),在父组件中使用 this.execute(context, nodeId) 进行动态路由。
SQL注入风险动态拼接SQL易引入安全漏洞。绝对禁止直接拼接值。使用上下文中的 ParamMapaddCondition 方法统一处理,确保所有值都作为预编译参数。
括号嵌套复杂的 AND/OR 逻辑需要正确的括号来保证运算优先级。在逻辑节点的 process 方法中统一管理括号。在进入时加 (,在执行完所有子节点后加 )
性能深度递归和大量组件执行可能带来的开销。1. LiteFlow 本身非常轻量。2. 可对解析后的SQL进行缓存(缓存key为规则树的JSON字符串或MD5)。

5. 总结与优势

通过将 LiteFlow 应用于动态 SQL 生成,我们实现了:

  1. 极致灵活:业务规则的变化只需更改规则树 JSON,无需修改代码和部署。
  2. 高度可维护:每个条件都是独立组件,修改、添加、复用非常简单(如添加 cond_email_like 组件)。
  3. 清晰直观:EL 规则和组件代码共同构成了清晰的“流程图”,可读性远胜于复杂的 if-else
  4. 安全可靠:统一的参数化处理机制从根本上杜绝了 SQL 注入。
  5. 强扩展性:未来新增函数计算(如 date_diff(...) > 7)、复杂表达式等,只需开发新组件并配置规则即可。

此方案是解决复杂动态查询需求的一个强大、优雅且面向未来的架构选择。