让我们说我在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
我可以亲自做这样的事情.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
(或者为我的非合成用例找到更好的名字)。
答案 0 :(得分:3)
当您使用includes
时,它会将这些已加载的记录存储在association_cache
中,您可以在控制台中查看这些记录。当您执行p = Person.includes(:vehicles)
时,它会将这些记录存储为密钥:vehicles
下的关联。它使用你在包含中传递它的任何键。
然后,当您致电p.cars
时,它会注意到:cars
中没有association_cache
密钥,必须查找它们。它没有意识到汽车被混合到:vehicles
键中。
要能够通过p.vehicles
或p.cars
访问缓存的汽车,需要在这两个密钥下缓存它们。
它存储的不只是一个简单的数组 - 它是一个关系。因此,您不能只在Hash中手动存储记录。
在你提出的解决方案中,我认为包含每个密钥可能是最简单的代码。 Person.includes(:cars, :trucks)
如果每个请求只执行一次,那么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
。当然,你仍然不会得到一个关系。
这不是那么漂亮,但我喜欢它包含在一个不依赖于任何其他方法的方法中。