I need to update attribute :average_rate when new comment is added.
I have in comments.rb
belongs_to :page, :counter_cache => true
and in page.rb
has_many :comments
after_save :update_average_rate
and update_average_rate method in page.rb
def update_average_rate(comment)
if comments_count_changed?
write_attribute :average_rate, (comments.sum(:rate) / comments.count.to_f).ceil
end
end
but it doesnt work. When I am doing
raise comments_count_changed?.inspect
in update_average_rate method, it outputs "false" , but comments_count is changed. What I am doing wrong? Thanks in advance
Your problem is that counter updates don't actually set the "changed" flags to true.
For a column/attribute a, a_changed? will be true if and only if a has been changed but not saved to the database. The basic behavior goes like this:
Load or create o. o.a_changed? will be false.
o.a = pancakes, o.a_changed? will be true.
o.save, o.a_changed? will be false.
You're using :counter_cache but internally, that uses update_counters and that:
simply does a direct SQL update for the record with the given ID, altering the given hash of counters by the amount given by the corresponding value
So after update_counters has been called, the counter attribute will not be marked as changed as the counter value in the database will be the new one.
I think you'll have to move your average_rate logic into an after_save callback on Comment.
Related
I have model that I want to set a boolean to false whenever it is changed. Taht way I can reprocess it later to make sure the related records are up to date. I've tried a few things, but keep getting myself into a loop.
/app/model/contest.rb
class Contest < ActiveRecord::Base
has_many :results, dependent: :destroy
after_update :set_contest_not_updated
def set_contest_not_updated
self.contest_updated=false
self.save
end
def set_contest_updated
self.update_column(:contest_updated_at, Time.now)
self.update_column(:contesy_updated, true)
end
Expected action:
contest.update_atrributes(flag_that_effects_scoring: true)
I would expect that is the above is run, the contest.contest_updated boolean would be set to false and only be set to true when the contest.set_contest_updated() method is run.
Of course calling save in the after_update callback will get you into a loop. It keeps saving over and over again until the end of time.
However, before_update should get the job done.
PS: Don't call save on *_save and *_update callbacks. This will always get you into loops.
before_update :set_contest_not_updated
def set_contest_not_updated
self.contest_updated = false
end
This will never work as when your after_update is called, it will invoke another after_update call and you will get the stack level too deep message.
The best solution is to have a foriegn key in another table/model such as contest_updates.
If you want your solution however, set the flag in a before_update filter
Recent days , I was trying to cache rails app use Redis store.
I have two models:
class Category < ActiveRecord::Base
has_many :products
after_save :clear_redis_cache
private
def clear_redis_cache
puts "heelllooooo"
$redis.del 'products'
end
end
and
class Product < ActiveRecord::Base
belongs_to :category, touch: true
end
in controller
def index
#products = $redis.get('products')
if #products.nil?
#products = Product.joins(:category).pluck("products.id", "products.name", "categories.name")
$redis.set('products', #products)
$redis.expire('products', 3.hour.to_i)
end
#products = JSON.load(#products) if #products.is_a?(String)
end
With this code , the cache worked fine.
But when I updated or created new product (I have used touch method in relationship) it's not trigger after_save callback in Category model.
Can you explain me why ?
Have you read documentation for touch method?
Saves the record with the updated_at/on attributes set to the current
time. Please note that no validation is performed and only the
after_touch, after_commit and after_rollback callbacks are executed.
If an attribute name is passed, that attribute is updated along with
updated_at/on attributes.
If you want after_save callbacks to be executed when calling touch on some model, you can add
after_touch :save
to this model.
If you set the :touch option to :true, then the updated_at or updated_on timestamp on the associated object will be set to the current time whenever this object is saved or destroyed
this the doc :
http://guides.rubyonrails.org/association_basics.html#touch
You can also (at least in rails 6) just use after_touch: callback
From the docs:
after_touch: Registers a callback to be called after a record is touched. See ActiveRecord::Callbacks for more information.
Using Rails 3.1.3 and I'm trying to figure out why our counter caches aren't being updated correctly when changing the parent record id via update_attributes.
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
end
class Event < ActiveRecord::Base
has_many :exhibitor_registrations, :dependent => :destroy
end
describe ExhibitorRegistration do
it 'correctly maintains the counter cache on events' do
event = Factory(:event)
other_event = Factory(:event)
registration = Factory(:exhibitor_registration, :event => event)
event.reload
event.exhibitor_registrations_count.should == 1
registration.update_attributes(:event_id => other_event.id)
event.reload
event.exhibitor_registrations_count.should == 0
other_event.reload
other_event.exhibitor_registrations_count.should == 1
end
end
This spec fails indicating that the counter cache on event is not being decremented.
1) ExhibitorRegistration correctly maintains the counter cache on events
Failure/Error: event.exhibitor_registrations_count.should == 0
expected: 0
got: 1 (using ==)
Should I even expect this to work or do I need to manually track the changes and update the counter myself?
From the fine manual:
:counter_cache
Caches the number of belonging objects on the associate class through the use of increment_counter and decrement_counter. The counter cache is incremented when an object of this class is created and decremented when it’s destroyed.
There's no mention of updating the cache when an object is moved from one owner to another. Of course, the Rails documentation is often incomplete so we'll have to look at the source for confirmation. When you say :counter_cache => true, you trigger a call to the private add_counter_cache_callbacks method and add_counter_cache_callbacks does this:
Adds an after_create callback which calls increment_counter.
Adds an before_destroy callback which calls decrement_counter.
Calls attr_readonly to make the counter column readonly.
I don't think you're expecting too much, you're just expecting ActiveRecord to be more complete than it is.
All is not lost though, you can fill in the missing pieces yourself without too much effort. If you want to allow reparenting and have your counters updated, you can add a before_save callback to your ExhibitorRegistration that adjusts the counters itself, something like this (untested demo code):
class ExhibitorRegistration < ActiveRecord::Base
belongs_to :event, :counter_cache => true
before_save :fix_counter_cache, :if => ->(er) { !er.new_record? && er.event_id_changed? }
private
def fix_counter_cache
Event.decrement_counter(:exhibitor_registration_count, self.event_id_was)
Event.increment_counter(:exhibitor_registration_count, self.event_id)
end
end
If you were adventurous, you could patch something like that into ActiveRecord::Associations::Builder#add_counter_cache_callbacks and submit a patch. The behavior you're expecting is reasonable and I think it would make sense for ActiveRecord to support it.
If your counter has been corrupted or you've modified it directly by SQL, you can fix it.
Using:
ModelName.reset_counters(id_of_the_object_having_corrupted_count, one_or_many_counters)
Example 1: Re-compute the cached count on the post with id = 17.
Post.reset_counters(17, :comments)
Source
Example 2: Re-compute the cached count on all your articles.
Article.ids.each { |id| Article.reset_counters(id, :comments) }
I recently came across this same problem (Rails 3.2.3). Looks like it has yet to be fixed, so I had to go ahead and make a fix. Below is how I amended ActiveRecord::Base and utilize after_update callback to keep my counter_caches in sync.
Extend ActiveRecord::Base
Create a new file lib/fix_counters_update.rb with the following:
module FixUpdateCounters
def fix_updated_counters
self.changes.each {|key, value|
# key should match /master_files_id/ or /bibls_id/
# value should be an array ['old value', 'new value']
if key =~ /_id/
changed_class = key.sub(/_id/, '')
changed_class.camelcase.constantize.decrement_counter(:"#{self.class.name.underscore.pluralize}_count", value[0]) unless value[0] == nil
changed_class.camelcase.constantize.increment_counter(:"#{self.class.name.underscore.pluralize}_count", value[1]) unless value[1] == nil
end
}
end
end
ActiveRecord::Base.send(:include, FixUpdateCounters)
The above code uses the ActiveModel::Dirty method changes which returns a hash containing the attribute changed and an array of both the old value and new value. By testing the attribute to see if it is a relationship (i.e. ends with /_id/), you can conditionally determine whether decrement_counter and/or increment_counter need be run. It is essnetial to test for the presence of nil in the array, otherwise errors will result.
Add to Initializers
Create a new file config/initializers/active_record_extensions.rb with the following:
require 'fix_update_counters'
Add to models
For each model you want the counter caches updated add the callback:
class Comment < ActiveRecord::Base
after_update :fix_updated_counters
....
end
A fix for this has been merged in to active record master
https://github.com/rails/rails/issues/9722
The counter_cache function is designed to work through the association name, not the underlying id column. In your test, instead of:
registration.update_attributes(:event_id => other_event.id)
try
registration.update_attributes(:event => other_event)
More information can be found here: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html
I'd like to create a callback function in rails that executes after a model is saved.
I have this model, Claim that has a attribute 'status' which changes depending on the state of the claim, possible values are pending, endorsed, approved, rejected
The database has 'state' with the default value of 'pending'.
I'd like to perform certain tasks after the model is created on the first time or updated from one state to another, depending on which state it changes from.
My idea is to have a function in the model:
after_save :check_state
def check_state
# if status changed from nil to pending (created)
do this
# if status changed from pending to approved
performthistask
end
My question is how do I check for the previous value before the change within the model?
You should look at ActiveModel::Dirty module:
You should be able to perform following actions on your Claim model:
claim.status_changed? # returns true if 'status' attribute has changed
claim.status_was # returns the previous value of 'status' attribute
claim.status_change # => ['old value', 'new value'] returns the old and
# new value for 'status' attribute
claim.name = 'Bob'
claim.changed # => ["name"]
claim.changes # => {"name" => ["Bill", "Bob"]}
Oh! the joys of Rails!
you can use this
self.changed
it return an array of all columns that changed in this record
you can also use
self.changes
which returns a hash of columns that changed and before and after results as arrays
For Rails 5.1+, you should use active record attribute method: saved_change_to_attribute?
saved_change_to_attribute?(attr_name, **options)`
Did this attribute change when we last saved? This method can be
invoked as saved_change_to_name? instead of
saved_change_to_attribute?("name"). Behaves similarly to
attribute_changed?. This method is useful in after callbacks to
determine if the call to save changed a certain attribute.
Options
from When passed, this method will return false unless the original
value is equal to the given option
to When passed, this method will return false unless the value was
changed to the given value
So your model will look like this, if you want to call some method based on the change in attribute value:
class Claim < ApplicationRecord
after_save :do_this, if: Proc.new { saved_change_to_status?(from: nil, to: 'pending') }
after_save :do_that, if: Proc.new { saved_change_to_status?(from: 'pending', to: 'approved') }
def do_this
..
..
end
def do_that
..
..
end
end
And if you don't want to check for value change in callback, you can do the following::
class Claim < ApplicationRecord
after_save: :do_this, if: saved_change_to_status?
def do_this
..
..
end
end
I recommend you have a look at one of the available state machine plugins:
acts_as_state_machine
alter_ego
Either one will let you setup states and transitions between states. Very useful and easy way of handling your requirements.
I've seen the question rise in many places, so I wrote a tiny rubygem for it, to make the code a little nicer (and avoid a million if/else statements everywhere): https://github.com/ronna-s/on_change.
I hope that helps.
You will be much better off using a well tested solution such as the state_machine gem.
I have models like this:
class Person
has_many :phones
...
end
class Phone
belongs_to :person
end
I want to forbid changing phones associated to person when some condition is met. Forbidden field is set to disabled in html form. When I added a custom validation to check it, it caused save error even when phone doesn't change. I think it is because a hash with attributes is passed to
#person.update_attributes(params[:person])
and there is some data with phone number (because form include fields for phone). How to update only attributes that changed? Or how to create validation that ignore saves when a field isn't changing? Or maybe I'm doing something wrong?
You might be able to use the
changed # => []
changed? # => true|false
changes # => {}
methods that are provided.
The changed method will return an array of changed attributes which you might be able to do an include?(...) against to build the functionality you are looking for.
Maybe something like
validate :check_for_changes
def check_for_changes
errors.add_to_base("Field is not changed") unless changed.include?("field")
end
def validate
errors.add :phone_number, "can't be updated" if phone_number_changed?
end
-- don't know if this works with associations though
Other way would be to override update_attributes, find values that haven't changed and remove them from params hash and finally call original update_attributes.
Why don't you use before_create, before_save callbacks in model to restrict create/update/save/delete or virtually any such operation. I think hooking up observers to decide whether you want to restrict the create or allow; would be a good approach. Following is a short example.
class Person < ActiveRecord::Base
#These callbacks are run every time a save/create is done.
before_save :ensure_my_condition_is_met
before_create :some_other_condition_check
protected
def some_other_condition_check
#checks here
end
def ensure_my_condition_is_met
# checks here
end
end
More information for callbacks can be obtained here:
http://guides.rubyonrails.org/activerecord_validations_callbacks.html#callbacks-overview
Hope it helps.