Rails 4.2: Eager-loading has_many relation with STI - ruby-on-rails

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_methods 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 selecting, not whereing. 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.

Related

implement each method in user class which has extend ActiveRecord::Base

Redefine each methods in ActiveRecord::Base for spec class User
This is what i know
class Rainbow
include Enumerable
def each
yield "red"
yield "orange"
yield "yellow"
yield "green"
yield "blue"
yield "indigo"
yield "violet"
end
end
r = Rainbow.new
r.select { |a| a.start_with?('r')} #=> ["red"]
Ok !!
Like this way what i want in User Model
class User < ActiveRecord::Base
include Enumerable
def user_ids
User.all.map(&:id) ## instead of this i want to write like User.map(&:id)
end
end
Actually There is lots of data in News model and in need only id from all the record To write the query like User.all.map(&:id) it taking lots of time.
1: For that i need to redefine each method but how ? but what line of codes i need to write in each method .
2: so that all the enumerable method can invoke on that classe`s object
Is there any other way.
Any help would be greatly appreciate.
This is not a good idea. The reason is because ActiveRecord classes (and therefore inner instance methods) can be both accessed as first-class object (when you call Model.foo) or via the ActiveRecord::Relation object and association proxy.
There is a very high chance that you will cause some hard-to-detect conflict at some point.
There is no real benefit of trying to do what you want to do. In fact, the method
class User < ActiveRecord::Base
include Enumerable
def user_ids
User.all.map(&:id)
end
end
can already be rewritten to
def user_ids
User.ids
end
that is a shorter version for
def user_ids
User.pluck(:id)
end
Note that both pluck and ids selects only the required field, hence they are way more efficient (both at Ruby level and at database level) than loading all the records and mapping a field.
Without mentioning that your code is probably wrong. In fact, you are defining an instance method that should be called
User.new.user_ids
whereas you probably expect to use it as
User.user_ids
hence you can define it as
class User < ActiveRecord::Base
def self.user_ids
# User it's implicit, its the current scope
ids
end
end
You can define each
class User < ActiveRecord::Base
include Enumerable
def each(&block)
# all returns a lazy-evaluated scope
# that responds to each
# Note that .each will trigger a query.
# In this case, that's effectively equivalent to to_a.each
all.each(&block)
end
end
but it will not bring you any advantage. Moreover, that will always trigger a query at the time you call the method, skipping the very handy lazy-load feature of active record.
In fact, ActiveRecord::Relation exists also as a performance improvement to take advantage of lazy-load.
Bottom line, if your goal is to not type User.all.map(&:id) then use a custom method, or use the Rails API effectively.
User.all.map(&:id)
can be written as
User.pluck(:id)
which is equivalent to
User.ids
that wrapped in a method becomes
class User
def self.user_ids
ids
end
end
User.user_ids

Get data through association

Tables:
User
Project has_many Results
Project has_many Data through ProjectData
Results belongs_to data, project
In the Result table I have a column :position of type int.
So I would like to get all the results with a level < 50, actually the value of count.
I am thinking in adding in the Result class
def get_top_level current_user
tsum = []
Project.where(user_id: current_user).each do |project|
tsum << project.results.where("level <= ?", 50).count
end
return sum(tsum)
end
This will work, but I feel that there should be a easy and prettier way of doing this.
And is it ok to user the class name in a view and pass different values for example:
<%=Results.get_top_level(current_user)%>
Or
<%=#results.get_top_level(current_user)%>
If none of those are a good practice, can you help me with a alternative solution for this.
Thank you.
I would create a method on the project model something like this.
def get_top_level
self.results.select{ |result| result.level <= 50 }
end
On the user model. What's the relationship here, does a user have many projects? Or just one project.
def get_top_level
self.top_level_projects.inject(:+)
end
def top_level_projects
self.projects.map(&:get_top_level)
end
Now when you call current_user.get_top_level
This will find the top_level_projects, map the associated results and add them all together.

Avoid scope hitting database if association already loaded

I have 2 models like so:
class Country < ActiveRecord::Base
has_many :cities
end
class City < ActiveRecord::Base
belongs_to :country
scope :big, where("population > 1000000")
end
Then, in the code I load a country with it's cities, like so:
country = Country.include(:cities).find(id)
But when I execute:
country.cities.big
It makes a hit to the db with this query:
SELECT * FROM cities where country_id = 1 AND population > 1000000
Which works fine, but it's not necessary since the cities where all already loaded by the :include.
Is there a way to tell the scope to not hit the db if the association is already loaded?
I can do it with an association extension, but not for a regular scope. On extensions I do something like:
has_many :cities do
def big
if loaded?
detect {|city| city.population > 1000000}
else
where("population > 1000000")
end
end
end
But this would be repeating the scope in 2 places and I want to reuse the scope on the city model.
The scope logic uses methods that work with Arel under the hood, and ruby Enumerables don't know how to use them. You may be able to refactor your logic to an abstraction that can be translated to use either the Arel or Enumerable methods, but this won't always be possible:
def self.build_scope(abstracted)
where(abstracted.map(&:to_s).join(' '))
end
def self.build_enum(abstracted)
select{|city| city.send(abstracted[0]).send(*abstracted[1..2]) }
end
def self.abstract_big
[:population, ">", 10000]
end
scope :big_scope, build_scope(abstract_big)
def self.big_enum
build_enum abstract_big
end
You could then do:
country.cities.big_enum
A much better idea would be to only eagerly load according to the scope that you want (if you know it in advance):
country = Country.include(:cities).merge(City.big).find(id)

Inspect object with associations

I have two models where A has_many B. If I load A including associated B as such:
a = A.find(:first, include: :bs)
a.inspect only shows the attributes of a:
=> "#<A id: 1, name: \"Test\", created_at: \"2012-07-02 21:50:32\", updated_at: \"2012-07-02 21:50:32\">"
How can I do a.inspect such that it displays all associated a.bs?
You can't do that by default. It might create too many problems and side effects with inspecting objects. However you could extend inspect yourself with something like this:
class A < ActiveRecord::Base
...
def inspect
[super, bs.inspect].join("\n")
end
end
Note though that that's not very clever, since it will force the loading of bs every time you inspect an A instance. So maybe you want to be smarter and do something like this:
def inspect
[super, bs.loaded? ? bs.inspect : nil].compact.join("\n")
end
This will only inspect bs if it's already preloaded (with :include for example).
Or maybe you want to create a super_inspect instead that does everything automatically. You could extend ActiveRecord::Base with something like:
class ActiveRecord::Base
def deep_inspect
([inspect] + self.class.reflect_on_all_associations.map { |a|
self.send(a.name).inspect
}).compact.join("\n ")
end
end
This will automatically look up all the associations with reflect_on_all_associations, and if the association is loaded it will call inspect on that.
Now you can modify the above code however you want to create your own customized inspect, or just extend the current inspect if you like. Anything is possible with a little bit of code.
Here is an example of an updated version that is a bit smarter:
class ActiveRecord::Base
def deep_inspect
([inspect] + self.class.reflect_on_all_associations.map { |a|
out = ""
assoc = self.send(a.name)
# Check for collection
if assoc.is_a?(ActiveRecord::Associations::CollectionProxy)
# Include name of collection in output
out += "\n#{assoc.name.pluralize}:\n"
out += self.send(a.name).to_a.inspect
else
out += self.send(a.name).inspect
end
out
}).compact.join("\n ")
end
end
Along the same line as the answer from #Casper, here is a helper method that marshals all associations down the dependency chain:
# app/models/application_record.rb
#
# placing the helper in the ApplicationRecord superclass
# allows all application models to inherit the helper
class ApplicationRecord < ActiveRecord::Base
def self.marshal
# collect the names of the objects associations
single_associations = self.class.reflect_on_all_associations(:has_one ).map {|x| x.name}
plural_associations = self.class.reflect_on_all_associations(:has_many).map {|x| x.name}
# serialize the object as a JSON-compatible hash
self.as_json.merge(
# merge in a hash containing each `has_one` association via recursive marshalling
# the resulting set of associated objects are merged into
# the original object's serialized hash, each keyed by the name of the association
single_associations.reduce({}) { |memo, assoc| memo.merge({ assoc => self.send(assoc).marshal }) }.as_json
).merge(
# merge in the `has_many` associations
# the resulting set of associated collections must then be processed
# via mapping each collection into an array of singular serialized objects
plural_associations.reduce({}) { |memo, assoc| memo.merge({ assoc => self.send(assoc).map {|item| item.marshal } }) }.as_json
)
end
end
You would then be able to call this helper method by calling:
Marshal.serialize a
This is not quite the same as an inspection, since it is actually serializing the object into a hash structure, but it will give you similar information.
Note that the possible associations are separated are separated into two groups: singular associations (which reference a single target object), and plural associations (which are ActiveRecord CollectionProxy objects, i.e. they are Enumerable). Because we are serializing associated objects as hashes, each has_many association must be parsed as a collection of individually serialized objects (e.g. we map each association within the collection as its serialized form).
The belongs_to association should be ignored, as mapping associations in both directions would immediately create a circular dependency graph. If you wish to marshal along the "chain of belonging" instead, you could do something like
def self.trace
parent_associations = obj.class.reflect_on_all_associations(:belongs_to).map {|x| x.name}
obj.as_json.merge single_associations.reduce({}) { |memo, assoc| memo.merge({ assoc => obj.send(assoc).trace }) }.as_json
end

At what level in a model object does ActiveRecord not load associated objects

I have a couple of models that are composites of multiple objects. I basically manage them manually for saves and updates. However, when I select items out, I don't have access to the associated properties of said item. For example:
class ObjectConnection < ActiveRecord::Base
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |value, key|
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
return r
end
end
and each item:
class Item < ActiveRecord::Base
has_many :assets, :as => :assetable, :dependent => :destroy
When I use the ObjectLocation.find_three_by_location_id, I don't have access to assets whereas if use Item.find(id) in most other situations, I do.
I tried using includes but that didn't seem to do it.
thx
Sounds like the simplest solution would be to add methods to your ObjectConnection model for easy access like so:
class ObjectConnection < ActiveRecord::Base
def engine
Engine.find(engine_id)
end
def chassis
Chassis.find(chassis_id)
end
# rest of class omitted...
I'm not exactly sure what you're asking... If this doesn't answer what you're asking, then can you try to be a little bit more clear with what exactly you are trying to accomplish? Are the Chassis and Engine mdoels supposed to be polymorphic associations with your Item model?
Also, the code you're using above won't work due to the fact that you are trying to dynamically set properties on a model. It's not your calls to Item.find that are failing, it's your calls to value[:engine_item]= and value[:chassis_item] that are failing. You would need to modify it to be something like this if you wanted to keep that flow:
def self.get_three_by_location_id location_id
l=ObjectConnection.find_all_by_location_id(location_id).first(3)
r=[]
l.each_with_index do |obj_conn, key|
# at this point, obj_conn is an ActiveRecord object class, you can't dynamically set attributes on it at this point
value = obj_conn.attributes # returns the attributes of the ObjectConnection as a hash where you can then add additional key/value pairs like on the next 2 lines
value[:engine_item]=Item.find(value.engine_id)
value[:chassis_item]=Item.find(value.chassis_id)
r << value
end
r
end
But I still think that this whole method seems unnecessary due to the fact that if you setup proper associations on your ObjectConnection model to begin with, then you don't need to go and try to handle the associations manually like you're attempting to do here.

Resources