“过滤”急切加载的数据时出现问题

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 社区中的一个已知问题,在我使用 joinswhere 子句时会发生这种情况。 您可以指定包含条件,但不建议这样做。它将为您提供所需的 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 基本上只是wheretake 的包装器,它确实会进行数据库调用。

完成您正在寻找的另一种方法是简单地将集合代理视为一个数组,使用 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 [实现过滤器]中将多个表与雄辩和急切的加载一起加入

过滤数据时出现logstash grok问题

通过数据视图过滤数据表时出现问题(使用 Concat 字段)

尝试过滤提取的数据库信息时出现卸载组件警告

使用 Servlet 过滤器和 j_security_check 登录时出现无限循环

Linq to Entities - 使用 Include() 急切加载