I have a Post model which has many ratings and I would like to store the average ratings figure for each post in the post ratings db column. I have a counter cache on the post model which counts the number of ratings.
I have the following code in my post.rb
def update_rating
if self.ratings_count_changed?
self.rating = self.ratings.average(:rating)
end
end
I had this as a before_save callback so whenever the post is edited and saved it updates the ratings, not very useful.
Is there a way I can call this method whenever a new rating is created?
One way of getting closer to your goal is to add after_create callback to Rating model:
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_attributes(:rating => self.post.ratings.average(:rating))
end
end
Or even more OO friendly version:
class Post < ActiveRecord::Base
has_many :ratings
# ...
def update_average_rating
update_attributes(:rating => self.ratings.average(:rating))
end
end
class Rating < ActiveRecord::Base
belongs_to :post
# ...
after_create :update_post_average_rating
def update_post_average_rating
self.post.update_average_rating
end
end
Do you think about storing average rating in rating model instead post model? In that case you don't need any callback and you need recalc average by first request after changes.
Instead of before_save on post, do an after_create on the rating because it sounds like you need to update that rating score when a new rating is created, not before the post is saved.
How about putting this into Rating model:
after_create :update_post_average_rating
def update_post_average_rating
self.post.rating = #....
end
Related
I have the following models:
class Post < ApplicationRecord
has_one: :basic_metric
has_one: :complex_metric
end
class BasicMetric < ApplicationRecord
belongs_to :post
end
class ComplexMetric < ApplicationRecord
belongs_to :post
end
Once a post is created, both basic_metric and complex_metric are nil:
p = Post.first
p.basic_metric # => nil
p.complex_metric # => nil
And because of how my app is going to work, the BasicMetricsController and ComplexMetricsController only have the update method. So I would like to know if there is a way to create them as soon as a post is created.
One very common way of accomplishing this is using ActiveRecord callbacks
class Post
after_create :create_metrics
private
def create_metrics
# methods created by has_one, suggested in the comments
create_basic_metric(additional_attrs)
create_complex_metric(additional_attrs)
end
end
Another option you have is to overwrite the method created by has_one, i.e.:
class Post
has_one: :basic_metric
has_one: :complex_metric
def basic_metric
super || create_basic_metric(additional_attrs)
end
def complex_metric
super || create_complex_metric(additional_attrs)
end
end
This way they won't be created automatically with any new post, but created on demand when the method is called.
Can you try this one,
post = Post.first
post.build_basic_metric
post.build_complex_metric
This will help you to build/create the has_one association object if record not saved by default use post.save at the end.
If you need this in modal you can use like this,
class Post
after_create :build_association_object
private
def create_metrics
self.build_basic_metric
self.build_complex_metric
# save # (optional)
end
end
I have 2 models: User and Favorite. In model Favorite:
class Favorite < ApplicationRecord
belongs_to :user, foreign_key: :user_id
def self.add_favorite(options)
create!(options)
end
def self.unfavorite(options)
where(options).delete_all
end
Now, I want to limit number of records saved to Favorite is 10. It mean that users are only liked 10 products. I researched google, someone said that I try to use callback and I think it's right way, but it raise 2 questions:
1. Can I use query in method for callback?
2. Callback can be pass argument?
It is sample code I think:
class Favorite < ApplicationRecord
after_create :limit_records(user_id)
belongs_to :user, foreign_key: :user_id
def self.add_favorite(options)
create!(options)
end
def self.unfavorite(options)
where(options).delete_all
end
def limit_records(user_id)
count = self.where(user_id: user_id).count
self.where(used_id: user_id).last.delete if count > 10
end
If user had 10 favorite, when they like any products, callback will be called after Favorite is created and will be delete if it's 11th record.
You have:
belongs_to :user, foreign_key: :user_id
in your Favorite model and limit_records is an instance method on Favorite. So you have access to the user as self.user_id (or just user_id since self is implied) inside limit_records and there is no need for an argument:
after_create :limit_records
def limit_records
# same as what you have now, `user_id` will be `self.user_id`
# now that there is no `user_id` argument...
count = self.where(user_id: user_id).count
self.where(used_id: user_id).last.delete if count > 10
end
In Rails is there a way that I can make a method call itself based on a change in the database? For instance, lets say I have two classes: Products and Orders.
Orders have three possible enum values:
class Order < ActiveRecord::Base
enum status: [:pending, :processing,:shipped]
belongs_to :products
end
I would like to batch process Orders so when a product has 50 orders, I want it to set all Orders associated with it to processed. Orders default to :pending. To change an order to :processing I would call order.processing!. I could write a method into the Products model like:
def process_orders
if self.orders.count=50
self.orders.each do |order|
order.processing!
end
end
The problem with this is that I would have to call the process_orders method for it to execute, is there anyway I could make it automatically execute once a product has 50 orders?
This is sounds like a good opportunity to use an Active Record Callback.
class Order < ActiveRecord::Base
belongs_to :product
after_save do
product.process_orders if product.pending_threshold_met?
end
end
class Product < ActiveRecord::Base
has_many :orders
def pending_threshold_met?
orders.where(status: :pending).count >= 50
end
end
I think you can use update_all to update the status column of all of your orders at once rather looping through them one by one:
self.orders.update_all(status: :processing)
and wrap that inside a callback.
Something like this:
class Order < ActiveRecord::Base
after_save do
product.process_orders if product.has_fifty_pending_orders?
end
# rest of your model code
end
class Product < ActiveRecord::Base
# rest of your model code
def process_orders
self.orders.update_all(status: :processing)
end
def has_fifty_pending_orders?
self.orders.where(status: :pending).count >= 50
end
end
Suppose I have a Rails app that deals with Posts and Comment objects. A Post has_many Comments and each Comment belongs_to a Post.
Each Comment has a word_count property. The Post object has an average_comment_word_count property which is an average of each of the Comment's word_count.
First question is if the Post object gets modified asynchronously (comments get added which affects the average word count), at what point should I recalculate the property? When the object is returned? Or each time a new comment is added? Does it go into the comment or post helper methods? Which controller function should call this method?
Also when I include the following Post helper method, I get a NULL value returned as JSON.
def average_word_count
#average_word_count = 0
# current_user returns the current user object
# user has_many posts and each post belongs_to a user
current_user.posts.find(params[:id]).comments.each do |comment|
#average_word_count += comment.word_count / current_user.posts.find(params[:id]).comments.count
end
#average_word_count
end
class Comment < ActiveRecord::Base
belongs_to :post
after_save :update_post_word_count
def update_post_word_count
average_wc = post.comments.average(:word_count)
post.update_attributes average_comment_word_count: average_wc
end
end
Or, derive it only when you need it:
class Post < ActiveRecord::Base
has_many :comments
def average_comment_word_count
comments.average :word_count
end
end
Or, if it's just used once somewhere with low traffic, brazenly flout the Law of Demeter and just calculate it as needed from a post object:
Average Comment Word Count: <%= #post.comments.average :word_count %>
Update: As #coreward notes, the first part of this answer isn't useful for asynchronous updates, but the rest of the answer may still be helpful.
You would be a lot better off just building a custom counter cache based on what's already in ActiveModel that keeps track of the total number of words, then just count comments to do math manually.
# you need a comments_count column and a words_count column in this table
class Post < ActiveRecord::Base
has_many :comments
def avg_words_per_comment
words_count / comments_count
end
end
class Comment < ActiveRecord::Base
belongs_to :post, :counter_cache => true
after_save { update_counters(post.id, :words => word_count }
before_destroy { update_counters(post.id, :words => -word_count }
end
# And in your view:
<p>
The average comment for this post has <%= #post.avg_words_per_comment %> words.
</p>
Then you don't need to worry about asynchonicity and the calculation on view is minimal.
https://github.com/rails/rails/blob/master/activerecord/lib/active_record/counter_cache.rb#L65
I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..