Rails 4.2:使用STI预加载has_many关系

5

假设我在Rails中有一个与使用STI的表相关的关系,就像这样:

class Vehicle < ActiveRecord::Base; end

class Car < Vehicle; end

class Truck < Vehicle; end

class Person < ActiveRecord::Base
  has_many :cars
  has_many :trucks
  has_many :vehicles
end

我想在一次查询中加载一个人及其所有的汽车和卡车,但这种方式不起作用:

# Generates three queries
p = Person.includes([:cars, trucks]).first

...这个很接近,但是没有运气:

# Preloads vehicles in one query
p = Person.includes(:vehicles).first
# and this has the correct class (Car or Truck)
p.vehicles.first
# but this still runs another query
p.cars

我可以在person.rb中做类似以下的操作:
def cars
  vehicles.find_all { |v| v.is_a? Car }
end

但是Person#cars不再是一个集合代理,而我喜欢集合代理。

有没有优雅的解决方案?

编辑:将此添加到Person中可使我使用一个查询获取所需的数组项;这非常接近我想要的:

def vehicle_hash
  @vehicle_hash ||= vehicles.group_by {|v|
    v.type.tableize
  }
end

%w(cars trucks).each do |assoc|
  define_method "#{assoc}_from_hash".to_sym do
    vehicle_hash[assoc] || []
  end
end

现在我可以执行 Person.first.cars_from_hash (或者为我的非合成用例找到更好的名称)。

1个回答

4
当您使用includes时,它会将这些加载的记录存储在association_cache中,您可以在控制台中查看。当您执行p = Person.includes(:vehicles)时,它会将这些记录存储为一个关联,存储在键:vehicles下。它使用您在includes中传递的任何键。
因此,当您调用p.cars时,它注意到在association_cache中没有:cars键,并且必须去查找它们。它不知道汽车被混合到了:vehicles键中。
要能够通过p.vehiclesp.cars访问缓存的汽车,需要在这两个键下都缓存它们。
它存储的不仅仅是一个简单的数组 - 它是一个Relation。因此,您不能手动将记录存储在哈希表中。
在您提出的解决方案中,我认为包含每个键可能是最简单的 - 从代码上来说。如果您每次请求只执行一次,则3个SQL语句并不那么糟糕。
如果性能是一个问题,我认为最简单的解决方案很像您建议的。我可能会编写一个新的方法find_all_cars而不是覆盖关系方法。
尽管如此,我可能会覆盖vehicles并允许它接受一个类型参数:
def vehicles(sti_type=nil)
  return super unless sti_type
  super.find_all { |v| v.type == sti_type }
end

编辑

你可以通过Rails缓存vehicles,因此你可能只需要依赖它。你的define_method也可以这样做:

%w(cars trucks).each do |assoc|
  define_method "preloaded_#{assoc}" do
    klass = self.class.reflect_on_all_associations.detect { |assn| assn.name.to_s == assoc }.klass
    vehicles.select { |a| a.is_a? klass }
  end
end

即使您不使用includes,第一次调用它时,它将缓存关联 - 因为您是在select而不是where。当然,您仍然不会得到一个Relation。
虽然并不是非常美观,但我喜欢它只包含一个方法且不依赖于其他方法的特点。

是的,性能损失可能会很明显;我在为两个父对象上的六个STI类执行此操作。嗯...如果要为association_cache创建实际关系,需要做些什么呢? - Nate
我已在顶部添加了您解决方案的一种变体。模糊地说,这样做是否合理?不过很好奇:为什么要更改车辆以采用 STI 类型?(在我的情况下,我通常会为每个请求加载所有的 STI 类型。) - Nate
看起来还不错。虽然你仍然没有得到一个关系作为结果。我正在寻找一种解决方案,它添加了最少的方法和最少的复杂性。你的解决方案似乎增加了一点点太多,超出了我的口味。我也会添加一个编辑。 - evanbikes
谢谢。是的,我真的希望能够以某种方式告诉 AR“使用一个查询加载所有这些 STI 子类”,并获得正确的关系,但我想这实际上不可能。 - Nate

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