Разобрать JSON в запрос SQL с AREL - PullRequest
0 голосов
/ 28 августа 2018

Я создаю механизм правил, в котором некоторые из правил являются логическими предикатами поверх низкоуровневых правил "если-то".

Другими словами, у меня есть таблица соединений, в которой хранятся совпадения между пользователями и правилами, которым эти пользователи были удовлетворены. После обработки правил более низкого порядка механизм оценивает правила более высокого уровня и создает новые соответствия для пользователей, которые их удовлетворяют.

'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
         ]
      }
   ]
}

Проблема, как вы можете видеть, заключается в том, что эти правила могут быть вложены бесконечным количеством способов. Так что мне, наверное, нужен рекурсивный цикл здесь. Не могу придумать один. Надеюсь, что вы, ребята, поможете мне и дадите мне хотя бы руководство о том, как преобразовать это псевдо-AST-дерево JSON в фактическое AST-дерево Arel.

Спасибо!

ОБНОВЛЕНИЕ: Оказывается, вы не можете использовать одну и ту же таблицу дважды в операторе SQL, см. Ответ ниже.

1 Ответ

0 голосов
/ 29 августа 2018

Похоже, grouping метод - это способ создания объекта Arel, который можно передать в Arel::SelectManager. Вот что я придумал:

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)
...