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
答案 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