使用 AREL 将 JSON 解析为 SQL 查询

Parse JSON into an SQL query with AREL

我正在构建一个规则引擎,其中一些规则是较低级别 "if-then" 规则之上的逻辑谓词。

换句话说,我有一个连接 table,它存储用户和发现这些用户满足的规则之间的匹配项。处理低阶规则后,引擎会评估更高级别的规则并为满足这些规则的用户创建新的匹配项。

'user_matches':
+---------+---------+-----------+
| rule_id | user_id | rule_type |
+---------+---------+-----------+
| 1       | 1       | simple    |
+---------+---------+-----------+
| 2       | 1       | simple    |
+---------+---------+-----------+
| 1       | 2       | simple    |
+---------+---------+-----------+
| 3       | 1       | compound  |
+---------+---------+-----------+

在上面的示例中,规则 #3 要求用户同时满足规则 #1 和规则 #2。用户 #1 匹配此规则,用户 #2 不匹配。

因为我想避免多次访问数据库,所以我需要一种方法将此类谓词转换为普通的 SQL 查询,然后将其传递给 Arel::InsertManager。这是它的要点:

UserMatch.select(UserMatch.arel_table[:user_id])
  .where(UserMatch.arel_table[:rule_id].eq(1)
  .and(UserMatch.arel_table[:rule_id].eq(2))).to_sql

=> SELECT "user_matches"."user_id" 
FROM "user_matches" 
WHERE ("user_matches"."rule_id" = 1 
AND "user_matches"."rule_id" = 2)

我将规则树存储为 JSON:

{  
   "or":[  
      {  
         "and":[  
            {  
               "or":[  
                  21,
                  42
               ]
            },
            84
         ]
      },
      {  
         "or":[  
            168,
            336
         ]
      }
   ]
}

如您所见,问题在于这些规则可以以无数种方式嵌套。所以我可能需要一个递归循环。想不出一个。希望你们帮帮我,至少给我一个方向,告诉我如何将这个 JSON 伪 AST 树解析为实际的 Arel AST 树。

谢谢!

更新: 原来你不能在 SQL 语句中使用相同的 table 两次,请参阅下面的答案。

看起来 grouping 方法是创建可以传递给 Arel::SelectManager 的 Arel 对象的方法。这是我想出的:

require 'rails_helper'
RSpec.describe CompositeProcessor do

  MUTEX = Mutex.new

  before (:each) do
    @joins = json_tree.to_s.scan(/\D(\d+)\D/).map.with_index{ |item,i| i+1 }
  end

  def parse (tree)
    if Hash === tree
      raise 'There can be only one operator' if tree.keys.size != 1
      operator, operands = tree.to_a.first
      raise 'Operands should be in an array' if operands.class != Array
      raise 'There should be at least two operands' if operands.size < 2
      operands = parse(operands)
      query = operands.shift
      operands.each do |operand|
        query = query.send(operator, operand)
      end
      tree = UserMatch.arel_table.grouping(query)
    elsif Array === tree
      tree.map! do |item|
        case item
        when Hash
          parse(item)
        when Integer
          MUTEX.synchronize do
            id = @joins.shift
            UserMatch.arel_table.alias("t#{id}")[:rule_id].eq(item)
          end
        end
      end
    end
    return tree
  end

  context "being passed a JSON tree of predicates" do
    let(:json_tree) { { "or":[ { "and":[ { "or":[ 21, 42 ] }, 84 ] }, { "or":[ 168, 336 ] } ] } }
    it "parses it into valid arel AST" do
      expect(parse(json_tree).to_sql).to eq('((("t1"."rule_id" = 21 OR "t2"."rule_id" = 42) AND "t3"."rule_id" = 84) OR ("t4"."rule_id" = 168 OR "t5"."rule_id" = 336))')
    end
  end
end

此解决方案需要一个实例变量:因为每个条件都应在新的自连接别名(t1、t2 等)上进行评估,所以我们需要存储尚未处理的连接数量。

最后的 SQL 看起来像这样:

SELECT DISTINCT t1.user_id
FROM user_matches as t1 
INNER JOIN user_matches AS t2 ON (t1.user_id = t2.user_id)
INNER JOIN user_matches AS t3 ON (t2.user_id = t3.user_id)
WHERE t1.rule_id = 1 
AND (t2.rule_id = 2
  OR t3.rule_id = 4)