Переписать запрос ActiveRecord как рекурсивный SQL - PullRequest
3 голосов
/ 25 марта 2020

У меня есть древовидная структура активной записи с самоссылающимся объектом - например, объект может быть родительским или дочерним для другого объекта того же класса. Мне нужен способ эффективно отобразить эту структуру в коде. До сих пор я делал это в ruby с активной записью ORM, и это ужасно неэффективно.

Вот как выглядит модель pod.rb:

    has_many :pod_parents, class_name: "PodPod", dependent: :delete_all
    has_many :parents, through: :pod_parents, :foreign_key => 'parent_id', :source => 'parent'
    has_many :pod_children, class_name: "PodPod", :foreign_key => 'parent_id'
    has_many :children, through: :pod_children, :source => 'pod'

    scope :active, -> {
        where(pod_state: "active").where(pod_type: ["standard","readonly"])
    }

Вот соответствующая схема базы данных:

table "pods"
  t.string "intention"
  t.integer "user_id"
  t.string "slug"
  t.string "url_handle"
  t.index ["slug"], name: "index_pods_on_slug"
  t.index ["url_handle"], name: "index_pods_on_url_handle"

table "pod_pods"
  t.integer "parent_id"
  t.integer "pod_id"
  t.index ["parent_id", "pod_id"], name: "index_pod_pods_on_parent_id_and_pod_id", unique: true
  t.index ["parent_id"], name: "index_pod_pods_on_parent_id"
  t.index ["pod_id"], name: "index_pod_pods_on_pod_id"

А вот конкретные c функции, над которыми я работаю над оптимизацией:

def get_all_parents
    parents = []
    self.parents.active.each do |parent|
        parents << parent
        parents.concat(parent.get_all_parents)
    end
    return parents
end

def get_all_children
    children = []
    self.children.each do |child|
        children.concat(child.get_all_children)
    end
    return children
end

def get_all_parents_and_children
    pod_array = self.get_all_parents
    pod_array.concat(self.get_all_children)
    return pod_array
end

def get_all_relations(inclusive = false)
    circles_array = self.get_all_parents
    circles_array.each do |parent|
        circles_array = circles_array.concat(parent.get_all_children)
    end
    circles_array = circles_array.concat(self.get_all_children)
    unique_ids = circles_array.compact.map(&:id).uniq - [self.id]
    circles = Pod.where(id: unique_ids)
end

Насколько я смог исследовать Postgres поддерживает тип рекурсивного запроса SQL. Я использовал эти статьи, чтобы указать путь: 1 , 2 .

И это насколько я получил:

def get_all_parents2
      sql =
        <<-SQL
            WITH RECURSIVE pod_tree(id, path) AS (
                SELECT id, ARRAY[id]
                FROM pods
                WHERE id = #{self.id}
            UNION ALL
                SELECT pods.id, path
                FROM pod_tree
                JOIN pods ON pods.id=pod_tree.id
                JOIN pod_pods ON pod_pods.parent_id = pods.id
                WHERE NOT pods.id = ANY(path)
            )
            SELECT * FROM pod_tree
            ORDER BY path;
        SQL
      sql.chomp
        Pod.find_by_sql(sql)
    end

Мой SQL не особенно хорош, и я не понимаю, как перемещаться по древовидной структуре вверх и вниз, чтобы можно было переписать функции, которые я упомянул выше, как рекурсивные SQL. Буду признателен за помощь в этом. Спасибо.

Ответы [ 2 ]

3 голосов
/ 01 апреля 2020

То, что вы пытаетесь выполнить, sh определенно возможно с помощью рекурсивных CTE. Я расскажу о первых двух сценариях ios, которые у вас есть, так как два других являются просто продолжением первых двух.

Во всех SQL примерах я собираюсь использовать идентификатор 1 для иллюстрации значения что вы заменяете на уровне модели. Поскольку вы написали этот запрос, я собираюсь немного познакомиться с рекурсивными CTE и попытаться найти решение.

get_all_children

Давайте сначала рассмотрим метод get_all_children. Этот метод включает в себя переход по дереву, уровень за уровнем и покрытие узлов, с которыми мы сталкиваемся.

Так как pod_pods содержит всю информацию, касающуюся иерархии, и в получении дочерних элементов нет никакой вовлеченной области, мы можем просто использовать pod_pods для дети.

-- Snippet #1
WITH RECURSIVE pod_tree AS (
  SELECT pod_id -- Get the pod_id of the children of the base case node
  FROM pod_pods
  WHERE parent_id = 1 -- Base case
  UNION ALL -- Recurse on this and do a union with the previous step
  SELECT p.pod_id
  FROM pod_pods p
    INNER JOIN pod_tree ptree 
      ON ptree.pod_id = p.parent_id -- Get the children nodes for nodes found at the previous recursion step.
)

SELECT * FROM pods 
WHERE id IN (SELECT DISTINCT(pod_id) FROM pod_tree);

Ваш код Ruby не покрывает вероятность бесконечного l oop, происходящего из-за цикла, но если есть вероятность, что это может произойти, то, как вы будете решать это отслеживая идентификаторы, которые вы уже видели.

-- Snippet #2
WITH RECURSIVE pod_tree(pod_id, rtree) AS ( -- Extra rtree parameter to keep track of visited nodes
  SELECT pod_id, ARRAY[pod_id] -- Make the base case array with pod_id
  FROM pod_pods
  WHERE parent_id = 1 -- Base case
  UNION ALL
  SELECT p.pod_id, rtree || p.pod_id -- Add the current pod_id to array
  FROM pod_pods p
    INNER JOIN pod_tree ptree 
      ON ptree.pod_id = p.parent_id
  WHERE NOT (p.pod_id = ANY(rtree)) -- Exclude nodes which have already been seen  
)

SELECT * FROM pods 
WHERE id IN (SELECT DISTINCT(pod_id) FROM pod_tree);

Если у вас могут быть бесхозные отношения в pod_pods и вы хотите их игнорировать, тогда необходимо объединение между модулями.

-- Snippet #3
WITH RECURSIVE pod_tree(id, rtree) AS (
  SELECT p1.id, ARRAY[p1.id]
  FROM pods p1 INNER JOIN pod_pods p2 ON p1.id = p2.pod_id 
  WHERE parent_id = 1
  UNION ALL
  SELECT p1.id, rtree || p1.id
  FROM pods p1 
    INNER JOIN pod_pods p2 ON p1.id = p2.pod_id
    INNER JOIN pod_tree ptree ON p2.parent_id = ptree.id
  WHERE NOT (p1.id = ANY(ptree.rtree))  
)

SELECT * FROM pods WHERE id IN (SELECT DISTINCT(id) FROM pod_tree);

Если у вас нет бесхозных ссылок, я бы посоветовал вам go с фрагментом № 1 или № 2, так как они будут быстрее, чем № 3, так как это требует дополнительных объединений.

get_all_parents

Сначала для простоты добавим поля области действия, которые добавляются из-за активности позже. Сначала мы просто спустимся по дереву таблицы pod_pods, чтобы получить все параметры. вводит идентификаторы, а затем мы применяем область действия.

-- Snippet #4
WITH RECURSIVE pod_tree AS (
  SELECT parent_id -- Get the parent_id of the parents of the base case node
  FROM pod_pods
  WHERE pod_id = 1 -- Base case
  UNION ALL -- Recurse on this and do a union with the previous step
  SELECT p.parent_id
  FROM pod_pods p
    INNER JOIN pod_tree ptree 
      ON ptree.parent_id = p.pod_id -- Get the parent nodes for nodes found at the previous recursion step.
)

SELECT * FROM pods 
WHERE 
  id IN (SELECT DISTINCT(parent_id) FROM pod_tree)
  AND pod_state = 'active'
  AND pod_type IN ('standard', 'readonly')
;

Однако активный фильтр применяется только после того, как все узлы выбраны. Это не может быть идеальным, так как может пройти больше дерева, чем требуется, и даже может вернуть родителей узлов, которые не активны. Чтобы сделать так, как это делает метод в коде Ruby, нам нужно объединить его с модулями. Я добавляю здесь шаг, позволяющий избежать бесконечной рекурсии, и теперь у вас есть представление об этом.

-- Snippet #5
WITH RECURSIVE pod_tree(id, rtree) AS (
  SELECT p1.id, ARRAY[p1.id]
  FROM pods p1 
    INNER JOIN pod_pods p2 ON p1.id = p2.parent_id 
  WHERE pod_id = 1
    AND p1.pod_state = 'active' 
    AND p1.pod_type IN ('standard', 'readonly')
  UNION ALL
  SELECT p1.id, rtree || p1.id
  FROM pods p1 
    INNER JOIN pod_pods p2 ON p1.id = p2.parent_id
    INNER JOIN pod_tree ptree ON p2.pod_id = ptree.id
  WHERE p1.pod_state = 'active' 
    AND p1.pod_type IN ('standard', 'readonly')
    AND NOT (p1.id = ANY(ptree.rtree))  
)

SELECT * FROM pods WHERE id IN (SELECT DISTINCT(id) FROM pod_tree);

В Rails, основанном на вашем методе-заглушке, код для фрагмента # 5 будет выглядеть как

def get_all_parents
  sql =
    <<-SQL
      WITH RECURSIVE pod_tree(id, rtree) AS (
        SELECT p1.id, ARRAY[p1.id]
        FROM pods p1 
          INNER JOIN pod_pods p2 ON p1.id = p2.parent_id 
        WHERE pod_id = #{self.id}
          AND p1.pod_state = 'active' 
          AND p1.pod_type IN ('standard', 'readonly')
        UNION ALL
        SELECT p1.id, rtree || p1.id
        FROM pods p1 
          INNER JOIN pod_pods p2 ON p1.id = p2.parent_id
          INNER JOIN pod_tree ptree ON p2.pod_id = ptree.id
        WHERE p1.pod_state = 'active' 
          AND p1.pod_type IN ('standard', 'readonly')
          AND NOT (p1.id = ANY(ptree.rtree))  
      )

      SELECT * FROM pods WHERE id IN (SELECT DISTINCT(id) FROM pod_tree);
    SQL
  # IMP!
  # sql = sql_sanitize(sql)
  # Add some sanitize step here
  sql.chomp
  Pod.find_by_sql(sql)
end

Это должно охватывать ваши первые два варианта использования. Как упоминалось ранее, два других являются расширением этих двух, так что вы можете просто использовать их, чтобы расширить их.

Примечание:

  • Если у вас нет циклов, Вы можете избежать бесконечных столбцов рекурсии, так как это дополнительная бухгалтерия.
  • Если у вас нет бесхозных ссылок, предпочтите итерацию только для pod_pods для детей, поскольку это позволяет избежать ненужных объединений
  • rtree в указанных выше sql запросах содержится иерархия. Вы можете передать его обратно, если вам нужна эта информация. Я пропустил его, так как вы все равно в итоге сгладили результат.
  • Я выбирал уникальные узлы. Ваш код Rails в настоящее время будет извлекать несколько вхождений узла, если он посещается несколько раз. Если вы хотите это, а также порядок дерева, вы можете иметь следующее поведение:
-- Example for getting all parents
WITH RECURSIVE pod_tree(id, slug, pod_type, parent_id, rtree) AS (
  SELECT p1.id, p1.slug, p1.pod_type, p2.parent_id, ARRAY[p1.id] -- Select the fields you need
  FROM pods p1 INNER JOIN pod_pods p2 ON p1.id = p2.parent_id 
  WHERE pod_id = 1
  AND p1.pod_state = 'active' AND p1.pod_type IN ('standard', 'readonly')
  UNION ALL
  SELECT p1.id, p1.slug, p1.pod_type, p2.parent_id, rtree || p1.id
  FROM pods p1 INNER JOIN pod_pods p2 ON p1.id = p2.parent_id
  INNER JOIN pod_tree ptree ON p2.pod_id = ptree.id
  WHERE p1.pod_state = 'active' AND p1.pod_type IN ('standard', 'readonly')
  AND NOT (p1.id = ANY(ptree.rtree))  
)

SELECT * FROM pod_tree;

0 голосов
/ 25 марта 2020

Советую взглянуть на модель вложенного набора реализации дерева. В Rails уже есть гем, который понимает, что logi c awesome_nested_set

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...