Let's say I have a relation in Rails to a table that uses STI like:
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
... and I want to load a Person and all of its cars and trucks in one query. This doesn't work:
# Generates three queries
p = Person.includes([:cars, trucks]).first
... and this is close, but no luck here:
# 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
I could do something like this in person.rb:
def cars
vehicles.find_all { |v| v.is_a? Car }
end
but then Person#cars
isn't a collection proxy anymore, and I like collection proxies.
Is there an elegant solution to this?
EDIT: Adding this to Person gives me the items I want in arrays with one query; it's really pretty close to what I want:
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
and now I can do Person.first.cars_from_hash
(or find a better name for my non-synthetic use case).
When you use includes
, it stores those loaded records in the association_cache
, which you can look at in the console. When you do p = Person.includes(:vehicles)
, it stores those records as an association under the key :vehicles
. It uses whatever key you pass it in the includes.
So then when you call p.cars
, it notices that it doesn't have a :cars
key in the association_cache
and has to go look them up. It doesn't realize that Cars are mixed into the :vehicles
key.
To be able to access cached cars as either through p.vehicles
OR p.cars
would require caching them under both of those keys.
And what it stores is not just a simple array—it's a Relation. So you can't just manually store records in the Hash.
Of the solutions you proposed, I think including each key is probably the simplest—code-wise. Person.includes(:cars, :trucks)
3 SQL statements aren't so bad if you're only doing it once per request.
If performance is an issue, I think the simplest solution would be a lot like what you suggested. I would probably write a new method find_all_cars
instead of overwriting the relation method.
Although, I would probably overwrite vehicles
and allow it to take a type argument:
def vehicles(sti_type=nil)
return super unless sti_type
super.find_all { |v| v.type == sti_type }
end
EDIT
You can get vehicles
cached by Rails, so you probably can just rely on that. Your define_method
s could also do:
%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
Even if you don't use includes
, the first time you call it, it will cache the association—because you're select
ing, not where
ing. You still won't get a Relation back, of course.
It's not really that pretty, but I like that it's contained to one method that doesn't depend on any other ones.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With