“过滤”预先加载的数据时出现问题

时间:2014-03-04 17:57:06

标签: sql ruby-on-rails ruby ruby-on-rails-4 eager-loading

我正在使用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">]>

...

上面的输出表明热切加载正在按预期工作:运行ActiveRecord::Associations::CollectionProxy时加载article.comments对象后没有N + 1问题。

但是,当我尝试像下面这样运行代码时(请注意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查询。

所以,我的疑问/怀疑是:

  1. 为什么在最后一种情况下find_by子句使得急切加载不起作用(注意:即使在我通过使用除{{1}以外的子句“过滤”article.comments的情况下也会发生这种情况}})?
  2. Ruby on Rails应该将find_by个对象中已加载的数据作为数组处理,以免它碰到数据库吗?!
  3. 如何在最后一种情况下解决问题以避免N + 1问题?

2 个答案:

答案 0 :(得分:1)

我怀疑find_by是硬连接来进行数据库调用。

第一个示例中列出的对象属于CollectionProxy类型,这意味着您仍然可以对它们进行SQL查询。由于find_by是ActiveRecord的一部分,因此在Proxy类上调用它应该转到数据库。

我怀疑如果你改变你的代码以在评论集合上使用诸如find_all的可枚举方法那么你应该没问题,但这不是很有效(find_all在线性时间内运行)

或者,通过执行以下操作将所有内容转换为单个联接查询:

Article.joins(:comments).where(comments: {title: "My Title")

或者,如果您需要所有文章是否有匹配的评论,您可以简单地为原始评论添加条件:

Article.includes(:comments).where(comments: {title: "My Title")

答案 1 :(得分:1)

只是为了确认: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可能是一个很好的资源来看待,但在急切加载的情况下,说实话,我不相信它是可能的。如果这是一个问题,您可能会遇到我之前提到的数组方法。