Caching for model in rails - ruby-on-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

Related

Count nested item before save

Let have Order and Item models.
class Order < ApplicationRecord
has_many :items, inverse_of: :order, dependent: :delete_all
before_save do
self.packed_volume = compute_packed_volume
end
private
def compute_packed_volume
items.count * 0.1O
end
end
And
class Item < ApplicationRecord
belongs_to :order, inverse_of: :items
end
The problem is that items.count is equals to 0 since items are not yet created.
How can we get the number of items that will be created to used it when we create an order?
Try size instead, it won't run a query
items.size * 0.1O
Hope that helps!
What you're looking for is a "counter cache". It implements exactly what you're trying to do. ActiveRecord can do this for you:
belongs_to :post, :counter_cache => true
There are a couple of gems that do this, updating a count field in the parent record whenever a child record is created or deleted.
Gem counter-cache does the job simply. Another, counter-culture takes it a bit further, including support for counting children in has_many :through relationships.
Your example is a little more interesting, as you're not looking for the count, but a computation on the count. So, you could either just roll with that and use it to compute the packed_volume on the fly, probably in a method on the model very similar to your compute_packed_volume() method.
If you wanted to store the actual volume in your parent record (perhaps it is very expensive to compute), you need to shift from putting callbacks on the parent model to putting them on the child model. The counter_culture gem supports that with its "Delta Magnitude". Something like this:
class Item < ActiveRecord::Base
belongs_to :order
counter_culture :order, column_name: :packed_volume, delta_magnitude: 0.1
end
class Order < ActiveRecord::Base
has_many :items
end
The delta_magnitude parameter can take a proc, so you can do more complicated things. Perhaps something like this:
counter_culture :order, column_name: :packed_volume, delta_magnitude: -> {|item| item.length * item.width * item.height }
You could roll your own solution along these lines if you have other requirements that preclude using a gem. You'll need to add callbacks to Item for after_create and before_destroy to increment/decrement the parent record. You may also need to update the order when the record is changed, if your volume computation becomes more complicated.

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.

Total sum of has_many associations

Not new to Ruby on Rails, but never really worked with more complicated ActiveRecord queries.
Say I have a Affiliate model that has_many referred users and referred users has_many purchased_products.
What I want to do is an efficient ActiveRecord way of getting the total sum of the count of purchased_products of all the referred users. How do I go about doing this?
Thanks.
Assuming objects like:
class Affiliate < ActiveRecord::Base
has_many :users
end
class Users < ActiveRecord::Base
#should have purchased_products_count integer column
belongs_to :affiliate
has_many :pruchased_products
end
class PurchasedProducts < ActiveRecord::Base
belongs_to :user, counter_cache: :purchased_products_count
end
products_count = User.first.purchased_products.size # uses counter_cache to get the size
another_products_count = User.first.purchased_products_count # get the value diretly
all_users_products_count = my_affiliate.users.map(&:purchased_products_count).inject(:+) # makes an array of product counts then sums them
I think this might also work
my_affiliate.users.sum('purchased_products_count')

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?
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.

Specifying the default ordering for a has_many association using a join (Rails)?

I have a simple Customer model with a has many relationship with a Purchase model.
class Customer < ActiveRecord::Base
has_many :purchases
end
I am repeatedly finding that I need to order Customer.purchases in my views in the following way:
#customer.purchases.joins(:shop).order("shops.position").order(:position) #yes, two orders chained
In the interest of keeping things DRY, I'd like to put this somewhere centralized so I don't have to repeatedly do it. Ideally, I'd like to make it the default ordering for Customer.purchases. For example:
class Customer < ActiveRecord::Base
has_many :purchases, :order => joins(:shop).order("shops.position").order(:position)
end
Obviously the above doesn't work. How should I do this?
In your customer model you specified joins(:shop) is the value for the key :order. I think here is the problem, So you can use the joins as a key instead of order like below,
class Customer < ActiveRecord::Base
has_many :purchases, :joins => [:shop], :order => "shops.position"
end
I think it may work.
In your purchases model, you can create a class method:
Purchase.rb:
def self.order_by_position
joins(:shop).order("shops.position").order(:position)
end
Then you can say things like:
#customer.purchases.order_by_position
Purchase.order_by_position
You could create a method on Customer that returns ordered purchases:
class Customer < ActiveRecord::Base
has_many :purchases
def ordered_purchases
purchases.joins(:shop).order("shops.position").order(:position)
end
end
and call #customer.ordered_purchases from your views.

Resources