处理在活动记录范围内返回的记录

时间:2015-07-24 15:35:45

标签: ruby-on-rails rails-activerecord

TLDR :有没有办法定义一个范围,以便我可以操作在返回之前使用该范围的查询找到的记录?我是否可以使用查询返回的数据预先填充记录集合上的任意值,就像rails可以“预加载”关联数据一样?

基本上我有一个包含分层信息的数据库表,因此每一行都有一个父级,并且我有很多次在层次结构中上下链接以获得父节点或子节点。为了提高性能,我们正在大量使用Postgresql的WITH RECURSIVE查询,让我们快速获取给定节点ID集的所有死者。在我的实际模型中,我有两个使用这种查询的关键方法:实例方法descendants和范围find_with_all_descendants(*ids)。但是,如果我有这些模型的集合,并且我想通过调用descendants来遍历并获取每个模型的后代,我最终会为每条记录生成查询。所以我目前的代码看起来像这样

collection = Node.find_with_all_descendants(1,2,3,4)

# collection gets passed around to other parts of the program ...

collection.each do |node|
  # other parts of the program do stuff with node.descendants, resulting in    
  # a select N+1 issue as the query for descendants fires

  node.descendants
end

如果我可以调用Node.find_with_all_descendants(*ids)然后预填充后代集合会有什么好处,因此后续调用任何返回记录的descendants会触及缓存数据,而不是导致另一个查询。所以我的Node.descendants方法可能看起来像这样。

def descendants
  return @cached_descendants if @cached_descendants
  # otherwise execute big sql statement I'm not including 
end

然后我只需找到一个地方,我可以为使用@cached_descendants的查询返回的记录设置find_with_all_descendants

但鉴于这是一个范围,我可以返回的是一个活跃的记录关联,我不清楚如何设置这个缓存的值。在使用我的find_with_all_descendants范围的任何查询返回其记录后,我是否可以运行代码?

更新:按要求包含相关方法。还包括一些猴子修补魔法,我们用它来加载节点的深度和路径,以便完整。

scope :find_with_all_descendants, -> (*ids) do
  tree_sql =  <<-SQL
    WITH RECURSIVE search_tree(id, path, depth) AS (
        SELECT id, ARRAY[id], 1
        FROM #{table_name}
        WHERE #{table_name}.id IN(#{ids.join(', ')})
      UNION ALL
        SELECT #{table_name}.id, path || #{table_name}.id, depth + 1
        FROM search_tree
        JOIN #{table_name} ON #{table_name}.parent_id = search_tree.id
        WHERE NOT #{table_name}.id = ANY(path)
    )
    SELECT id, depth, path FROM search_tree ORDER BY path
  SQL

  if ids.any?
    rel = select("*")
      .joins("JOIN (#{tree_sql}) tree ON tree.id = #{table_name}.id")
      .send(:extend, NodeRelationMethods)
  else
    Node.none
  end
end

def descendants
  self.class.find_with_all_descendants(self.id).where.not(id: self.id)
end

# This defines the methods we're going to monkey patch into the relation returned by
# find_with_all_descendants so that we can get the path and the depth of nodes
module NodeRelationMethods
  # All nodes found by original ids will have a depth of 1
  # depth is accessible by calling node.depth
  def with_depth
    # Because rails is a magical fairy unicorn, just adding this select statement
    # automatically adds the depth attribute to the data nodes returned by this
    # scope
    select("tree.depth as depth")
  end

  def with_path
    # Because rails is a magical fairy unicorn, just adding this select statement
    # automatically adds the path attribute to the data nodes returned by this
    # scope
    self.select("tree.path as path")
  end
end

3 个答案:

答案 0 :(得分:1)

如果您在选择中添加path[1],则应该可以使用Ruby group_by AR group,这是对于SQL GROUP BY),按顶级父ID对所选记录进行分组。我已经在下面写了一个例子,并对范围进行了一些重构以利用链式范围:

def self.all_descendants
  tree_sql =  <<-SQL
    WITH RECURSIVE search_tree(id, path, depth) AS (
        SELECT id, ARRAY[id], 1
        FROM (#{where("1=1").to_sql}) tmp
      UNION ALL
        SELECT #{table_name}.id, path || #{table_name}.id, depth + 1
        FROM search_tree
        JOIN #{table_name} ON #{table_name}.parent_id = search_tree.id
        WHERE NOT (#{table_name}.id = ANY(path))
    )
    SELECT id, depth, path FROM search_tree ORDER BY path
  SQL
  unscoped.select("*, tree.depth as depth, tree.path as path, tree.path[1] AS top_parent_id")
    .joins("JOIN (#{tree_sql}) tree ON tree.id = #{table_name}.id")
end

def descendants
  self.class.where(id: id).all_descendants.where.not(id: id)
end

这样,您可以执行以下操作:

collection = Node.where(id: [1,2,3,4]).all_descendants
collection.group_by(&:top_parent_id).each do |top_parent_id, descendant_group|
  top_parent = descendant_group.detect{|n| n.id == top_parent_id}
  top_parent_descendants = descendant_group - top_parent
  # do stuff with top_parent_descendants
end

答案 1 :(得分:1)

看起来可以通过覆盖http://apidock.com/rails/v3.2.3/ActiveRecord/Relation/exec_queries来完成。这里有一些示例代码归结为简单的本质

scope :find_with_all_descendants, -> (*ids) do
  #load all your records here...
  where(#...).extend(IncludeDescendants)
end

module IncludeDescendants

  def exec_queries
    records = super
    records.each do |r|
      #pre-populate/manipulate records here before returning
    end
  end 

end

在返回记录之前,rails基本上会调用Relation#exec_queries。通过扩展我们在范围内返回的关系,我们可以覆盖exec_queries。在overriden方法中,我们得到原始方法结果,进一步操作它们然后返回

答案 2 :(得分:0)

这远远超过你需要它的点,但是我遇到了一个非常类似的问题,我想知道是否查看了递归查询gem,或者当时它是否可用,以及是否在这种情况下,它会满足您的需求吗?我希望不要修补核心类,也不希望覆盖ActiveRecord中的方法,但这似乎是一个坚实的DSL风格的扩展,以解决我认为是一个相当普遍的问题:

https://github.com/take-five/activerecord-hierarchical_query