Victor
Victor

Reputation: 13388

Eager load in model?

I wonder if we could eager load in model level:

# country.rb
class Country < ActiveRecord::Base
  has_many :country_days

  def country_highlights
    country_days.map { |country_day| country_day.shops }.flatten.uniq.map { |shop| shop.name }.join(", ")
  end
end

# country_day.rb
class CountryDay < ActiveRecord::Base
  belongs_to :country
  has_many :country_day_shops
  has_many :shops, :through => :country_day_shops
end

# shop.rb
class Shop < ActiveRecord::Base    
end

Most of the times it's difficult to use .includes in controller because of some polymorphic association. Is there anyway for me to eager load the method country_highlights at the model level, so that I don't have to add .includes in the controller?

Upvotes: 1

Views: 1471

Answers (2)

Sean Hill
Sean Hill

Reputation: 15056

You can't "eager load" country_days from a model instance, but you can certainly skip loading them all together by using a has_many through:. You can also skip the extra map, too.

# country.rb
class Country < ActiveRecord::Base
  has_many :country_days
  has_many :country_day_shops, through: :country_days  #EDIT: You may have to add this relationship
  has_many :shops, through: :country_day_shops #And change this one to use the new relationship above.

  def country_highlights
    shops.distinct_names.join(", ")
  end
end

# country_day.rb
class CountryDay < ActiveRecord::Base
  belongs_to :country
  has_many :country_day_shops
  has_many :shops, :through => :country_day_shops
end

# shop.rb
class Shop < ActiveRecord::Base
  def self.distinct_names
    pluck("DISTINCT shops.name")  #Edit 2: You may need this instead of 'DISTINCT name' if you get an ambiguous column name error.
  end
end

The has_many through: will use a JOIN to load the associate shop records, in effect eager loading them, rather than loading all country_day records and then for each country_day record, loading the associated shop.

pluck("DISTINCT name") will return an array of all of the unique names of shops in the DB, using the DB to perform a SELECT DISTINCT, so it will not return duplicate records, and the pluck will avoid loading ActiveRecord instances when all you need is the string name.

Upvotes: 2

Thomas Klemm
Thomas Klemm

Reputation: 10856

Edit: Read the comments first

You could cache the end result (the joined string or text record in your case), so you'll not have to load several levels of records just to build this result.

1) Add a country_highlights text column (result might be beyond string column limits)

2) Cache the country_highlights in your model with a callback, e.g. before every save.

class Country < ActiveRecord::Base  
  has_many :country_days

  before_save :cache_country_highlights

  private

  def cache_country_highlights
    self.country_highlights = country_days.flat_map(&:shops).uniq.map(&:name).join(", ")
  end
end

Caching you calculation will invoke a little overhead when saving a record, but having to load only one instead of three model records for displaying should speed up your controller actions so much that it's worth it.

Upvotes: 1

Related Questions