Eager load in model? - ruby-on-rails

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?

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.

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.

Related

Rails: Update join table custom attribute when a new record is created

Lets say I have two models: Performance and Band, and to connect the two I have a join table called performers. My ActiveRecord models are setup as follows:
class Band < ApplicationRecord
has_many :performers
has_many :performances, through: :performers, dependent: :destroy
end
class Performance < ApplicationRecord
has_many :performers
has_many :bands, through: :performers, dependent: :destroy
end
class Performer < ApplicationRecord
belongs_to :band
belongs_to :performance
end
Now the tricky part. I have a custom attribute called permissions in the performers table that captures the permission levels (owner, editor, viewer) for a performer, which defines who can make changes to a performance. Which brings me to my question: when a band creates a new performance, how can I set a value on the join table during creation e.g.
def create
performance = Performance.new(performance_params)
# here I add a performance to a band's performances, which creates a new performer record
band.performances << performance
# what I would also like to do (at the same time if possible) is also define the permission level
# during creation something like but:
performer = band.performers.last
performer.permissions = 'owner'
performer.save
render json: serialize(performance), status: 200
end
Is there something in Rails that can let me modify a join tables attribute during creation of an association?
EDIT
For reference, right now I do:
def create
performance = Performance.new(performance_params)
performer = Performer.new
performer.band = Band.find(params[:band_id])
performer.permissions = 'owner'
performance.performers << performer
performance.save!
render json: serialize(performance), status: 200
end
But was wondering if there was something simpler.
You can use either Association Callbacks on has_many or add appropriate callback to Performer model as it's still created as a model even it's joining one.
Something like:
class Performer < ApplicationRecord
belongs_to :band
belongs_to :performance
before_create :set_permissions
private
def set_permissions
self.permissions = 'owner'
end
end

Caching for model in rails

product.rb
has_many :votes
vote.rb
belongs_to :product
Every time, i use sorting in my index controller:
index_controller.rb
def index
#products = Product.all.sort { |m| m.votes.count }
end
So, i think it would be good to cache votes count for each product (create additional column votesCount in products table)?
If yes, can i preform that using before_save and before_delete callbacks in vote.rb model?
Or what is the best practice method?
Give me some examples please.
I guess you are looking for counter_cache
The :counter_cache option can be used to make finding the number of belonging objects more efficient
Consider these models:
class Order < ActiveRecord::Base
belongs_to :customer, counter_cache: true
end
class Customer < ActiveRecord::Base
has_many :orders
end
With this declaration, Rails will keep the cache value up to date, and then return that value in response to the size method.
Although the :counter_cache option is specified on the model that includes the belongs_to declaration, the actual column must be added to the associated model. In the case above, you would need to add a column named orders_count to the Customer model

Rails: Query to get collection associated with collection

So I have these four classes:
class User < ActiveRecord::Base
has_many :water_rights
end
class WaterRight < ActiveRecord::Base
belongs_to :user
has_many :place_of_use_area_water_rights
has_many :place_of_use_areas, through: :place_of_use_area_water_rights
end
class PlaceOfUseAreaWaterRight < ActiveRecord::Base
belongs_to :place_of_use_area
belongs_to :water_right
end
class PlaceOfUseArea < ActiveRecord::Base
has_many :place_of_use_area_water_rights
has_many :water_rights, through: :place_of_use_area_water_rights
end
and I call User.first.water_rights and get a collection of WaterRights. My question is how do I get a collection of PlaceOfUseAreas associated with those WaterRights without doing something like this:
areas = []
water_rights.each do |wr|
areas << wr.place_of_use_areas
end
areas.flatten.uniq{ |a| a.id }
This works but it makes a new query for every single WaterRight. I'm looking for a way to make one query to get the collection of associated PlaceOfUseAreas.
You just want to get all associated PlaceOfUseAreas objects in single query, right?
If so, Rails have pretty single line solution for it:
PlaceOfUseArea.joins(:water_wights).uniq
Read more about joins method if you want more information.

How to build a has_many-alike association containing a list of various Models?

I have an Order which has many line_items. Only this is not a LineItem module, but a list of "things that act Orderable". E.g. Addon or Site.
class Order
attr_accessor :line_items
before_save :persist_line_items
private
def persist_line_items
#line_items.each {|li| li.save }
end
end
class Addon
belongs_to: order
end
class Site
belongs_to: order
end
Which can be used as:
order = Order.new
order.line_items << Addon.new(order: order)
order.line_items << Site.new(order: order)
But, now I want to load an Order and join the "associated" line_items. I
could load them in an after_initialize hook, and do an
Addon.find_by(order_id: self.id) but that quickly leads to a lot of
queries; where a JOIN would be more appropriate. In addition, I
currently miss the validations trickling up: when a normal has_many
related item is invalid the containing model will not be valid either:
order = Order.new(line_items: [an_invalid_line_item])
order.valid? #=> false
I am wondering if there is a way
to leverage ActiveRecords' has_many-relation to be used with a list of
different models.
I think that a polymorphic association should do the trick.
Would look like this:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :orderable, polymorphic: true
end
class Site < ActiveRecord::Base
has_many :line_items, as: :orderable
end
class Addon < ActiveRecord::Base
has_many :line_items, as: :orderable
end
It would use a join table, but i think this is actually a good thing. Otherwise you could use STI for your Addon and Site models, but that would not make a lot of sense in my regard.

How to create an association that sets join table attributes automatically?

I am totally confused about how I should go about "the rails way" of effectively using my associations.
Here is an example model configuration from a Rails 4 app:
class Film < ActiveRecord::Base
# A movie, documentary, animated short, etc
has_many :roleships
has_many :participants, :through => :roleships
has_many :roles, :through => :roleships
# has_many :writers........ ?
end
class Participant < ActiveRecord::Base
# A human involved in making a movie
has_many :roleships
end
class Role < ActiveRecord::Base
# A person's role in a film. i.e. "Writer", "Actor", "Extra" etc
has_many :roleships
end
class Roleship < ActiveRecord::Base
# The join for connecting different people
# to the different roles they have had in
# different films
belongs_to :participant
belongs_to :film
belongs_to :role
end
Given the above model configuration, the code I wish I had would allow me to add writers directly to a film and in the end have the join setup correctly.
So for example, I'd love to be able to do something like this:
## The Code I WISH I Had
Film.create!(name: "Some film", writers: [Participant.first])
I'm not sure if I'm going about thinking about this totally wrong but it seems impossible. What is the right way to accomplish this? Nested resources? A custom setter + scope? Something else? Virtual attributes? thank you!
I created a sample app based on your question.
https://github.com/szines/hodor_filmdb
I think useful to setup in Participant and in Role model a through association as well, but without this will work. It depends how would you like to use later this database. Without through this query wouldn't work: Participant.find(1).films
class Participant < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
class Role < ActiveRecord::Base
has_many :roleships
has_many :films, through: :roleships
end
Don't forget to give permit for extra fields (strong_parameters) in your films_controller.rb
def film_params
params.require(:film).permit(:title, :participant_ids, :role_ids)
end
What is strange, that if you create a new film with a participant and a role, two records will be created in the join table.
Update:
You can create a kind of virtual attribute in your model. For example:
def writers=(participant)
#writer_role = Role.find(1)
self.roles << #writer_role
self.participants << participant
end
and you can use: Film.create(title: 'The Movie', writers: [Participant.first])
If you had a normal has_and_belongs_to_many relationship i.e. beween a film and a participant, then you can create a film together with your examples.
As your joining model is more complex, you have to build the roleships separately:
writer= Roleship.create(
participant: Participant.find_by_name('Spielberg'),
role: Role.find_by_name('Director')
)
main_actor= Roleship.create(
participant: Participant.find_by_name('Willis'),
role: Role.find_by_name('Actor')
)
Film.create!(name: "Some film", roleships: [writer, main_actor])
for that, all attributes you use to build roleships and films must be mass assignable, so in a Rails 3.2 you would have to write:
class Roleship < ActiveRecord::Base
attr_accessible :participant, :role
...
end
class Film < ActiveRecord::Base
attr_accessible :name, :roleships
...
end
If you want to user roleship_ids, you have to write
class Film < ActiveRecord::Base
attr_accessible :name, :roleship_ids
...
end
Addendum:
Of cause you could write a setter method
class Film ...
def writers=(part_ids)
writer_role=Role.find_by_name('Writer')
# skiped code to delete existing writers
part_ids.each do |part_id|
self.roleships << Roleship.new(role: writer_role, participant_id: part_id)
end
end
end
but that makes your code depending on the data in your DB (contents of table roles) which is a bad idea.

Resources