Why does after_save not trigger when using touch? - ruby-on-rails

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.

Related

Update record in before_destroy and prevent its destruction

I am using Rails 3.2, Ruby 1.9.3.
I need to prevent a record from being destroyed and update it in the before_destroy callback.
Given two classes with the following associations
class Course:
has_many :attendants
...
class Attendant:
belongs_to :course
before_destroy :dont_really_destroy
...
I got a before_destroy callback in Attendant:
def dont_really_destroy
update_attribute :deleted_at, Time.now
false
end
The callback does in fact prevent the delete when I call the destroy method. However, the record is not updated. It seemed reasonable since I by returning false I might be aborting any update (I tried with update_column as well). However, somehow, it does work as expected when the attendant record is "destroyed" from its association's (Course) form, by setting a _destroy form element. The record is correctly updated with deteled_at set, and not destroyed.
I've tried debugging to see the if the instances are different when I try to destroy from the course form vs directly destroying the attendant but I cannot see any differences.
When I do it via the course form, the record is updated like this:
course.assign_attributes(params[:course], :without_protection => true)
...
course.save
Hi instead of using callbacks here, why don't you simply update the destroy action?
#AttendantsController.rb
def destroy
update_attribute :deleted_at, Time.now
head :ok
end
And you default scope in your model
class Attendant:
belongs_to :course
default_scope -> { where(deleted_at: nil) }
...
Hope this will help you.
You could also use ActAsParanoid Gem which introduces soft deletion for rails.
https://github.com/ActsAsParanoid/acts_as_paranoid

Cannot access children in after_create method in Rails 5

In my app I have bookings and passengers where one booking has many passengers.
After creating a new booking I want to send a notification to all passengers, so in my Booking model I have:
after_create :send_notification
def send_notification
self.passengers.each do |passenger|
#DO STUFF
end
end
This does not do anything and if I try
puts(self.passengers.count)
it returns 0.
When I user after_save everything works, so I assume that after_create it created the parent but not the children yet.
Problem is that I can't use after_save because this trigger also after updating.
Any ideas?
You can add condition for after_save:
after_save :send_notification, if: :id_changed?
# or :id_previously_changed? I don't remember if in after_save you can access dirty attributes
Change your callback to this:
after_save :send_notification, on: :create
So that your callback will fire after saving instead of creating the object, but will only occur when it's first created.

Skip rails counter_cache update

I have a model that uses rails' built-in counter_cache association to increment/decrement counts. I have a requirement wherein I need to disable this when I destroy the model for a specific situation. I have tried to do something like Model.skip_callback(:destroy, :belongs_to_counter_cache_after_update) but it doesn't seem to work as expected (i.e it still ends up decrementing the associated model). Any helpful pointers would be appreciated.
One option is to temporarily override the method responsible for updating the cache count in case of destroy.
For example if you have following two models
class Category < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :category, counter_cache: true
end
Now you can try to find the methods responsible for updating cache count with following
2.1.5 :038 > Product.new.methods.map(&:to_s).grep(/counter_cache/)
This shows all the product instance methods which are related to counter_cache, with following results
=> ["belongs_to_counter_cache_before_destroy_for_category", "belongs_to_counter_cache_after_create_for_category", "belongs_to_counter_cache_after_update_for_category"]
From the names of the methods it shows that
"belongs_to_counter_cache_after_create_for_category"
might be responsible for counter cache update after destroy.
So I decided to temporarily override this method with one fake method which doesn't do anything(to skip counter cache update)
Product.class_eval do
def fake_belongs_to_counter_cache_before_destroy_for_category; end
alias_method :real_belongs_to_counter_cache_before_destroy_for_category, :belongs_to_counter_cache_before_destroy_for_category
alias_method :belongs_to_counter_cache_before_destroy_for_category, :fake_belongs_to_counter_cache_before_destroy_for_category
end
Now if you will destroy any product object, it will not update counter cache in Category table.
But its very important to restore the actual method after you have run your code to destroy specific objects. To restore to actual class methods you can do following
Product.class_eval do
alias_method :belongs_to_counter_cache_before_destroy_for_category, :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :fake_belongs_to_counter_cache_before_destroy_for_category
end
To ensure that the methods definitions always restored after your specific destroy tasks, you can write a class method, that will make sure to run both override and restore code
class Product < ActiveRecord::Base
belongs_to :category, counter_cache: true
def self.without_counter_cache_update_on_destroy(&block)
self.class_eval do
def fake_belongs_to_counter_cache_before_destroy_for_category; end
alias_method :real_belongs_to_counter_cache_before_destroy_for_category, :belongs_to_counter_cache_before_destroy_for_category
alias_method :belongs_to_counter_cache_before_destroy_for_category, :fake_belongs_to_counter_cache_before_destroy_for_category
end
yield
self.class_eval do
alias_method :belongs_to_counter_cache_before_destroy_for_category, :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :real_belongs_to_counter_cache_before_destroy_for_category
remove_method :fake_belongs_to_counter_cache_before_destroy_for_category
end
end
end
Now if you destroy any product object as given following
Product.without_counter_cache_update_on_destroy { Product.last.destroy }
it will not update the counter cache in Category table.
References:
Disabling ActiveModel callbacks https://jeffkreeftmeijer.com/2010/disabling-activemodel-callbacks/
Temporary overriding methods: https://gist.github.com/aeden/1069124
You can create a flag to decide when callback should be run, something like:
class YourModel
attr_accessor :skip_counter_cache_update
def decrement_callback
return if #skip_counter_cache_update
# Run callback to decrement counter cache
...
end
end
so before you destroy your object of a Model, just set value for skip_counter_cache_update:
#object = YourModel.find(some_id)
#object.skip_counter_cache_update = true
#object.destroy
so it will not run decrement callback.

How do I invoke a method only when my object (model) is first created in Rails 5?

I'm using Rails 5. I want a method invoked on my model only when the model is first created. I have tried this ...
class UserSubscription < ApplicationRecord
belongs_to :user
belongs_to :scenario
def self.find_active_subscriptions_by_user(user)
UserSubscription.joins(:scenario)
.where(["user_id = ? and start_date < NOW() and end_date > NOW()", user.id])
end
after_initialize do |user_subscription|
self.consumer_key = SecureRandom.urlsafe_base64(10)
self.consumer_secret = SecureRandom.urlsafe_base64(25)
end
end
but I noticed this gets called every tiem I retrieve a model from a finder method in addition to its begin created. How can I create such functionality in my model?
You want to use after_create (from active record docs) or after_create_commit which was introduced in Rails 5 as a shortcut for after_commit :hook, on: :create.
after_create always executes after the transactions block whereas after_create_commit does so after the commit but within the same transactions block. These details likely don't matter here, but it's a new capability if you need that extra control for ensuring the model state is correct before you execute the after call.
Pyrce's answer is good. Another way is to keep the after_initialize method but only run if it's a new record:
after_initialize :set_defaults
def set_defaults
if self.new_record?
self.consumer_key = SecureRandom.urlsafe_base64(10)
self.consumer_secret = SecureRandom.urlsafe_base64(25)
end
end
(It's generally considered better to not override the after_initialize method. Instead provide the name of a method to run, as I did above.

Update another column if specific column exist on updation

I have a model which have two columns admin_approved and approval_date. Admin update admin_approved by using activeadmin. I want when admin update this column approval_date also update by current_time.
I cant understand how I do this.Which call_back I use.
#app/models/model.rb
class Model < ActiveRecord::Base
before_update 'self.approval_date = Time.now', if: "admin_approved?"
end
This assumes you have admin_approved (bool) and approval_date (datetime) in your table.
The way it works is to use a string to evaluate whether the admin_approved attribute is "true" before update. If it is, it sets the approval_date to the current time.
Use after_save callback inside your model.
It would be something like this:
after_save do
if admin_approved_changed?
self.approval_date = Time.now
save!
end
end
Or change the condition as you like!
You could set the approval_date before your model instance will be saved. So you save a database write process instead of usage of after_save where you save your instance and in the after_save callback you would save it again.
class MyModel < ActiveRecord::Base
before_save :set_approval_date
# ... your model code ...
private
def set_approval_date
if admin_approved_changed?
self.approval_date = Time.now
end
end
end
May be in your controller:
my_instance = MyModel.find(params[:id])
my_instance.admin_approved = true
my_instance.save

Resources