Rails 5 预加载并查找

3
我有三个模型,分别如下:
class Parent < ApplicationRecord
  has_many :children
  has_many :assets
end

class Child < ApplicationRecord
  belongs_to :parent
end

class Asset < ApplicationRecord
  belongs_to :parent
end

现在我需要通过父级查找属于子级的资产。而“资产”有一个资产类型列。因此,我需要执行以下操作:
Parent.first.children.each do |child|
  child.parent.assets.find_by(asset_type: "first").asset_value
end

我该如何避免N+1查询?
Rails版本:5.1.6
Ruby版本:2.3.4

1
我认为你在那个查询中没有N+1问题,除非你想做类似于Child.each do |child|的操作。 - luisenrike
是的,你说得对。我需要迭代子元素。抱歉我错过了它。已编辑帖子。谢谢。 - Arefin
2
也许这只是一个例子,但我不明白你为什么要说“Parent.first.children”,然后是“child.parent.assets”。由于孩子只有一个父母,所以你应该只说“Parent.first.assets.find_by(asset_type: 'first').asset_value”。 - KPheasey
@KPheasey 我猜这只是一个玩具示例,这个想法需要应用到一组“父级”中,并且每个子资产需要完成的工作比这里显示的要多。在我看来,保持问题简洁明了是很好的。 - Andrew Schwartz
是的,这只是一个例子。实际情况更为复杂。孩子患有STI。 - Arefin
1个回答

10

首先的问题是,添加find_by无论你预先加载了多少内容(至少截止到Rails 4)都会执行另一个查询。这是因为find_by实现的方式是生成更多的SQL语句。如果你想要预加载,你可以使用find代替,只要父对象下不含过多资源,那么这种方式就很好;但如果有许多资源和/或者这些资源是占用大量内存的大对象(请参见下面的备选方案),那么这种方式就不可取。

你可以通过以下方式来预加载资源:

parent.children.preload(:parent => :assets) each do |child|
# Will not execute another query
child.parent.assets.find{ |asset| asset.asset_type == "first" }

或者,您可以声明一个has_many :through关联:

class Child < ActiveRecord::Base
  belongs_to :parent
  has_many :assets, through: :parent
  ...
end

然后您可以简单地

parent.children.preload(:assets).each do |child|
# Will not execute another query
child.assets.find { |asset| asset.asset_type == "first" }
如果你想在数据库层面而非Ruby中执行查找操作,你可以定义一个作用域关联:
class Parent < ActiveRecord::Base
  has_one :first_asset, ->{ where asset_type: "first" }
  ...
end

你可以使用preload(:parent => :first_asset)来进行预加载。


你是一个英雄。这个想法帮助我最小化查询时间。谢谢。 - Arefin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接