加入后如何为返回的每条记录使用预加载数据?

How can I use eager loaded data after join for each record returned?

在我的代码中,我加入了 2 个模型(例如 Post 和评论),然后我渴望加载第三个模型(例如作者)。当我打印每条记录时,rails 为 Post 的第一个实例使用预先加载的模型数据,但为每个额外的 Post[=38= 实例触发额外的数据库查询].这是一个 n+1 的问题。

我不明白的是为什么我只能对每个 post 的第一条记录使用预先加载的数据,而不是连接返回的附加记录?

我看过类似的问题,但不认为这些解决的是完全相同的问题。其中包括:

  • why is this rails association loading individually after an eager load?
  • belongs_to association loaded individually even after eager loading
  • Eager loading not loading in rails,以及其他

我还查看了 rails 源代码,特别是 ActiveRecord::Associations::Preloader 并尝试在本地对其进行一些修改,以查看是否可以缩小问题所在或是否会发生这里。我原以为 class 可能是通过 id 去除了非唯一记录实例(我认为它在第 92 行)但是当我更改它时我的问题没有得到解决。

我确实偶然发现了这个 whacky edge case 这似乎适用,但在我的实际代码中我使用 find_by_sql 并且未能有效地实现它。

我创建了一个 gist with steps to reproduce the issue 我正在体验和得到的输出。任何帮助将不胜感激。

注意:这个示例是我能想到的最简单的设置,它演示了我在实际代码中遇到的相同问题。在我的示例案例中,我知道我可以急于加载评论和作者然而,在我的实际代码中有一个更复杂的连接并且需要连接。我无法从这个示例中急切加载 Comment 模型。

对于那些将来发现这个的人,这里是我发现的和我实现的。

简单的要点示例

对于我要点中概述的简单示例,我发现通过更改以下内容可以获得所需的结果。

要点:

# Triggers n+1 for author after first puts of an author.
posts = Post.joins(:comments).select("posts.*, comments.body").includes(:author)
posts.each do |x|
  puts "#{x.title} #{x.author.name} #{x.body}"
end

更改为:

# Works as expected.
posts = Post.joins(:comments, :author).select("posts.*, comments.body").includes(:author)
posts.each do |x|
  puts "#{x.title} #{x.author.name} #{x.body}"
end

通过加入 author,我得到了我想要的结果,无需额外查询。

更复杂的示例和实现

虽然上面解决了我用来演示我的问题的简单示例,但在我更复杂的现实世界问题中(如我的问题中所述)我无法实现这个额外的连接来解决问题。

相反,我实现了这个伟大的 Thoughbot post and in the rails handbook on github (Query Object Pattern and Decorator Pattern) 中概述的查询对象模式和装饰器模式。

这些模式让我获得了预先加载我想要的特定关联的所有好处,但对于我更复杂的查询。