使用 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)
我正在构建一个规则引擎,其中一些规则是较低级别 "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)