“过滤”急切加载的数据时出现问题
Posted
技术标签:
【中文标题】“过滤”急切加载的数据时出现问题【英文标题】:Trouble when "filtering" eager loaded data 【发布时间】:2014-04-06 10:18:31 【问题描述】:我正在使用 Ruby on Rails 4,我想了解为什么在急切加载过程中运行进一步的 SQL 查询,即使数据是急切加载的。也就是说,我有以下代码可以正确加载:comments
:
@articles = @current_user.articles.includes(:comments)
当上述代码运行时,我使用以下代码“跟踪”记录器中发生的事情:
@articles.each do |article|
logger.debug article.comments
end
然后记录器说:
Article Load (0.4ms) SELECT ...
Comment Load (0.5ms) SELECT ... WHERE `articles`.`id` IN (...)
#<ActiveRecord::Associations::CollectionProxy [#<Comment id: 1, title: "Hello A">, #<Comment id: 2, title: "Hello B">]>
#<ActiveRecord::Associations::CollectionProxy [#<Comment id: 3, title: "Hello A">, #<Comment id: 4, title: "Hello C">]>
#<ActiveRecord::Associations::CollectionProxy [#<Comment id: 5, title: "Hello D">, #<Comment id: 6, title: "Hello E">]>
...
以上输出表明预加载按预期工作:没有 N+1 问题,因为在运行 article.comments
时加载了 ActiveRecord::Associations::CollectionProxy
对象。
但是,当我尝试运行如下代码时(注意find_by
子句):
@articles.each do |article|
logger.debug article.comments.find_by(:title => "Hello A")
end
然后记录器说:
Article Load (0.4ms) SELECT ...
Comment Load (0.5ms) SELECT ... WHERE `articles`.`id` IN (...)
Comment Load (0.4ms) SELECT ... AND `comments`.`title` = 'HELLO A'
#<Comment id: 1, title: "Hello A">
Comment Load (0.4ms) SELECT ... AND `comments`.`title` = 'HELLO A'
#<Comment id: 3, title: "Hello A">
Comment Load (0.4ms) SELECT ... AND `comments`.`title` = 'HELLO A'
nil
...
以上输出表明预加载没有按预期工作:每条评论都会运行一个 SQL 查询。
所以,我的问题/疑问是:
-
为什么在最后一种情况下,
find_by
子句使急切加载不起作用(注意:即使在我使用find_by
以外的子句“过滤”article.comments
时也会发生这种情况)?
Ruby on Rails 是否应该将已加载到 ActiveRecord::Associations::CollectionProxy
对象中的数据作为数组处理以避免撞到数据库?!
如何解决问题以避免在最后一种情况下出现 N+1 问题?
【问题讨论】:
【参考方案1】:我怀疑find_by
是硬连线来进行数据库调用的。
第一个示例中列出的对象是 CollectionProxy
类型,这意味着您仍然可以对它们进行 SQL 查询。由于find_by
是 ActiveRecord 的一部分,因此在 Proxy 类上调用它应该转到 DB。
我怀疑如果您更改代码以在 cmets 集合上使用诸如 find_all
之类的 Enumerable 方法,那么您应该没问题,但这不是很有效(find_all
以线性时间运行)
或者,通过执行以下操作将所有内容汇总到一个连接查询中:
Article.joins(:comments).where(comments: title: "My Title")
或者,如果您需要所有文章,无论它们是否具有匹配的 cmets,您都可以简单地在原始包含中添加一个条件:
Article.includes(:comments).where(comments: title: "My Title")
【讨论】:
@current_user.articles.includes(:comments)
产生的 SQL 查询比 Article.joins(:comments).where(comments: title: "My Title")
更“难”。但是,使用您的提示@current_user.articles.joins(:comments).where(comments: title: "My Title")
会产生这个问题:如果没有找到一篇文章的评论,那么返回的数组将完全排除该文章。如果我没记错的话,这是 Rails 社区中的一个已知问题,在我使用 joins
和 where
子句时会发生这种情况。
您可以指定包含条件,但不建议这样做。它将为您提供所需的 LEFT OUTER JOIN(即,无论是否存在匹配的 cmets 都加载文章:guides.rubyonrails.org/…
请注意,“缺失”文章是设计使然:.join
使用 INNER JOIN,而 .includes
使用 LEFT OUTER JOIN。这更像是实现的一个怪癖而不是一个错误
我尝试了@current_user.articles.includes(:comments).where(comments: title: "My Title")
,但它仍然导致提到的问题:没有匹配cmets的文章没有被检索到。【参考方案2】:
只是为了确认:David Underwood 是正确的,find_by
将进行数据库调用。事实上,find_by
基本上只是where
和take
的包装器,它确实会进行数据库调用。
完成您正在寻找的另一种方法是简单地将集合代理视为一个数组,使用 find
方法,如下所示:
@articles.each do |article|
logger.debug article.comments.find |comment| comment.title == "Hello A"
end
更新:
我不得不承认,这个有点笨。
以您正在寻找的方式完成此操作的方法是添加另一个 has_many
关系,该关系专门包含您想要的过滤条件,如下所示:
class Article < ActiveRecord::Base
has_many :hello_A_comments, -> where(title: "Hello A") , class_name: "Comment"
# rest of class
end
然后,您可以使用这个新关联进行预加载,如下所示:
@articles = @current_user.articles.includes(:hello_A_comments)
这部分很重要:
您现在不是通过原始的:comments
关联方法访问关联,而是通过新的hello_A_comments
方法访问关联,如下所示:
@articles.first.hello_a_comments
不幸的是,如您所见,这种方法不是很动态,遗憾的是我不知道如何在急切加载的情况下允许关联中的可变条件。 This answer 可能是一个很好的资源,但在急切加载的情况下,老实说我不相信它是可能的。如果这是一个问题,您可能会被我之前提到的数组方法卡住。
【讨论】:
.find
只会返回第一个匹配的元素。 .find_all
将返回所有匹配的元素。
@user502052 好的,我明白你的问题了。我想我可以弄清楚一些事情,给我一点时间......
@Paul Richter - 你是对的。我只想找到一个。
即使我也有类似的问题,但我无法解决。
我会试一试的。不管怎么说,还是要谢谢你。我很感激你的努力,真的。以上是关于“过滤”急切加载的数据时出现问题的主要内容,如果未能解决你的问题,请参考以下文章
在laravel [实现过滤器]中将多个表与雄辩和急切的加载一起加入
通过数据视图过滤数据表时出现问题(使用 Concat 字段)