我有以下自我参照关联:
class Action < ActiveRecord::Base
# self referential association
has_many :action_parents
has_many :parents, through: :action_parents
has_many :action_children, class_name: 'ActionParent', foreign_key: 'parent_id'
has_many :children, through: :action_children, source: :action
…
def should_finish
should_start + duration
end
def should_start
# my_start is a field in db: if there are no parents (root) it will use this field
return my_start if parents.empty?
parents.map(&:should_finish).sort.last
end
end
我的问题是should_finish
和should_start
互相调用这一事实,即使我预先加载父母,也会导致许多查询:
Action.includes(:parents).last.should_finish
# a new query every time it checks for parents
有关如何缓存actions
和parents
的想法吗?
编辑 - 让我给出一些背景信息:
# actions table: actions_parents table:
# id | duration task_id | parent_id
# 1 | 5 2 | 1
# 2 | 10 3 | 1
# 3 | 20 4 | 2
# 4 | 15 4 | 3
#
# |--------------|
# | action 2 |
# |---------- >| duration: 10 |
# | |--------------|
# | |
# |--------------| |--------->|--------------|
# | action 1 | | action 4 |
# | duration: 5 | | duration: 15 |
# |--------------| |--------->|--------------|
# | |
# | |--------------|
# |----------->| action 3 |
# | duration: 20 |
# |--------------|
PS:没有循环依赖。
假设我有一个my_start
的{{1}}字段:
some day at 10:00:00
我认为可以使用# action | should_start | should_finish
# -------------------------------------
# 1 | 10:00:00* | 10:00:05
# 2 | 10:00:05 | 10:00:15
# 3 | 10:00:05 | 10:00:25
# 4 | 10:00:25** | 10:00:40
#
# * value from db since there is no parent
# ** should_finish of parent with latest should_finish (action 3)
答案 0 :(得分:1)
在我知道具体细节之前,我会抛弃一个狂野的,
假设父母结构中没有突出的循环,你无法通过缓存任何缓存整个表的任何东西来帮助自己,因为每次遇到父母时,你都会为每个动作实例点击不同的父母。没有缓存策略,包括rails one,将使您无需将整个数据集移动到缓存中。
事情是,你似乎想要做的事情对于关系数据库来说真的很难,而且似乎正是图数据库发明的原因(见What are graph databases & When to use a graph database&amp; Neo4j on Heroku)
除了转到图形数据库或缓存整个动作表之外,最好的办法是优化查询(使用pluck
)并可能将它们重写为PLSQL函数。
B计划是让您了解有关数据的知识,
should_start
,duration
和should_finish
中的值会发生变化吗?它改变了很多吗?Action
模型的数据库字段成为有意义,这样您每次查找时都不必遍历?
should_start
和should_finish
?编辑1
目前我看到的唯一解决方案是取消递归问题。试试这个:
在字符串/文本字段中存储父结构的ID,例如
[1,2,3]
,[1]
和[]
然后将ancestor_ids
数组映射到id => action
def ancestry_hash
@ancestry_hash ||= Hash[ Action.includes(:action_parents).where(id: ancestor_ids).map{|action| [action.id, action]} ]
end
然后重新实现递归查询以遍历此哈希而不是activerecord树,否则您将触发其他查询。类似的东西:
def should_finish(id = self.id)
should_start(id) + ancestry_hash[id].duration
end
def should_start(id = self.id)
# my_start is a field in db: if there are no parents (root) it will use this field
action = ancestry_hash[id]
return my_start if action.action_parents.empty?
action.action_parents.pluck(:parent_id).map{ |parent_id| should_finish(parent_id) }.sort.last
end
我没有测试代码,但我希望你能得到这个想法,它应该足够接近这个
答案 1 :(得分:1)
问题:
简而言之,您有2个问题。一个是你实际上试图预加载超过你需要的东西(?!?),另一个是由于逻辑的递归性质,Rails并不急于加载你真正需要的东西。
为了进一步解释,请考虑一下:
my_action.parents.map(&:parents).flatten.map(&:parents)
Rails会:
请注意,在这种情况下,急切加载第一级父项并没有多大意义,因为您只是从一个Action实例开始 - 而不是一个集合。调用.parents
将在一次传递中获取该操作的所有第一级父级(这无论如何都是急切加载)。
那么当您开始使用集合(ActiveRelation)而不是实例时会发生什么?
Action.some_scope.includes(:parents).map(&:parents)
在这种情况下,范围中包含的所有操作的父项将被急切加载。调用.map(&:parents)
将不会触发任何进一步的SQL调用,这就是使用includes()
进行热切加载的重点。但是,有两件事情会破坏这一目的的全部目的 - 你正在做两件事:/
首先,您的起点实际上并不是一系列操作,因为您正在立即调用.last
。因此,所有父母对所有行为的取得都毫无意义 - 你只需要最后一次行动。一!因此,Rails足够聪明,可以缩小范围,并且只会急切地加载“最后”的父母。行动。但是,在这种情况下,热切加载并没有多大好处,因为调用.parents
会在以后产生相同的单个查询。 (如果以后的操作需要更快地进行,那么提前加载会有很小的好处,在这种情况下实用性有限)。因此,无论是否使用.includes
语句,您只需执行一次查询即可获取“最后一次”的父母。动作。
更重要的是,你在每个父母身上递归调用.parents
,而Rails绝对不知道你打算这样做。此外,递归调用本质上是不可预先获取的(不知道一些技巧),所以实际上没有任何方法可以告诉ActiveRecord,或使用&#39; vanilla&#39;对于这个问题的SQL,走链子并找出需要哪些父母,直到它已经完成(使得这一点没有实际意义)。所有这些都会导致N + 1情况的噩梦,正如您所经历的那样。
一些解决方案:
有几种方法可以缓解或消除N + 1问题 - 按实现复杂性顺序排列:
Action.last.parents.includes(parents: {parents: :parents}) # grabs up to 4 levels
完全跳过SQL,将所有操作加载到由关联的child_id键入的Action数组的散列中,并使用非ActiveRecord方法使用简单的Ruby聚合所需的内容。随着数据的增长,这种情况会迅速恶化,但对你来说可能已经足够了 - 至少目前是这样。
使用可以提前确定祖先树的模式,并使用该实用程序来帮助进行此计算。 @bbozo给出的示例是一种方法 - 您还可以探索诸如祖先,acts_as_tree,awesome_nested_set,closure_tree等可能的宝石,以及可能帮助您解决此问题的其他方法。
使用实际上在单个调用中执行递归计算的数据库特定函数。 PostgreSQL,Oracle和MS-SQL具有此功能,而MySQL和SQLite则没有。这可能会让您获得最佳性能,但仅使用ActiveRecord查询界面进行编写可能很复杂。
答案 2 :(得分:0)
你有没有想过记住这个?
在模型中
def should_start
return my_start if parents.empty?
@should_start ||= parents.map(&:should_finish).sort.last
end