Skip rails counter_cache update - ruby-on-rails

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.

Related

Callback for Active Storage file upload

Is there a callback for active storage files on a model
after_update or after_save is getting called when a field on the model is changed. However when you update (or rather upload a new file) no callback seems to be called?
context:
class Person < ApplicationRecord
#name :string
has_one_attached :id_document
after_update :call_some_service
def call_some_service
#do something
end
end
When a new id_document is uploaded after_update is not called however when the name of the person is changed the after_update callback is executed
For now, it seems like there is no callback for this case.
What you could do is create a model to handle the creation of an active storage attachment which is what is created when you attach a file to your person model.
So create a new model
class ActiveStorageAttachment < ActiveRecord::Base
after_update :after_update
private
def after_update
if record_type == 'Person'
record.do_something
end
end
end
You normally have created the model table already in your database so no need for a migration, just create this model
Erm i would just comment but since this is not possible without rep..
Uelb's answer works but you need to fix the error in comments and add it as an initializer instead of model. Eg:
require 'active_storage/attachment'
class ActiveStorage::Attachment
before_save :do_something
def do_something
puts 'yeah!'
end
end
In my case tracking attachment timestamp worked
class Person < ApplicationRecord
has_one_attached :id_document
after_save do
if id_document.attached? && (Time.now - id_document.attachment.created_at)<5
Rails.logger.info "id_document change detected"
end
end
end
The answer from #Uleb got me 90% of the way, but for completion sake I will post my final solution.
The issue I had was that I was not able to monkey patch the class (not sure why, even requiring the class as per #user10692737 did not help)
So I copied the source code (https://github.com/rails/rails/blob/fc5dd0b85189811062c85520fd70de8389b55aeb/activestorage/app/models/active_storage/attachment.rb#L20)
and modified it to include the callback
require "active_support/core_ext/module/delegation"
# Attachments associate records with blobs. Usually that's a one record-many blobs relationship,
# but it is possible to associate many different records with the same blob. If you're doing that,
# you'll want to declare with <tt>has_one/many_attached :thingy, dependent: false</tt>, so that destroying
# any one record won't destroy the blob as well. (Then you'll need to do your own garbage collecting, though).
class ActiveStorage::Attachment < ActiveRecord::Base
self.table_name = "active_storage_attachments"
belongs_to :record, polymorphic: true, touch: true
belongs_to :blob, class_name: "ActiveStorage::Blob"
delegate_missing_to :blob
#CUSTOMIZED AT THE END:
after_create_commit :analyze_blob_later, :identify_blob, :do_something
# Synchronously purges the blob (deletes it from the configured service) and destroys the attachment.
def purge
blob.purge
destroy
end
# Destroys the attachment and asynchronously purges the blob (deletes it from the configured service).
def purge_later
blob.purge_later
destroy
end
private
def identify_blob
blob.identify
end
def analyze_blob_later
blob.analyze_later unless blob.analyzed?
end
#CUSTOMIZED:
def do_something
end
end
Not sure its the best method, and will update if I find a better solution
None of these really hit the nail on the head, but you can achieve what you were looking for by following this blog post https://redgreen.no/2021/01/25/active-storage-callbacks.html
I was able to modify the code there to work on attachments instead of blobs like this
Rails.configuration.to_prepare do
module ActiveStorage::Attachment::Callbacks
# Gives us some convenient shortcuts, like `prepended`
extend ActiveSupport::Concern
# When prepended into a class, define our callback
prepended do
after_commit :attachment_changed, on: %i[create update]
end
# callback method
def attachment_changed
record.after_attachment_update(self) if record.respond_to? :after_attachment_update
end
end
# After defining the module, call on ActiveStorage::Blob to prepend it in.
ActiveStorage::Attachment.prepend ActiveStorage::Attachment::Callbacks
end
What I do is add a callback on my record:
after_touch :check_after_touch_data
This gets called if an ActiveStorage object is added, edited or deleted. I use this callback to check if something changed.

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.

Why does after_save not trigger when using touch?

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.

overriding rails active record destroy unexpected deletion of has_and_belongs_to_many relationships

I have a Commentable class that inherits from ActiveRecord::Base and an Event class that inherits from Commentables.
I have overwritten the destroy methods in both of these classes, and Event.distroy calls super. However, some unexpected things happen. Specifically, the Event's has_and_belongs_to_many associations are deleted. I think this is happening because some modules are getting included between the Commentables and the Event class, but not sure if there is a way to stop this.
Here's the simplified code:
class Commentable < ActiveRecord::Base
has_many :comments
def destroy
comments.destroy_all
self.deleted = true
self.save!
end
end
class Event < Commentable
has_and_belongs_to_many :practitioners, :foreign_key => "commentable_id"
def destroy
#some Event specific code
super
end
end
I don't want to delete the rows from the database, just set a "deleted" flag. Nor do I want to delete any of the associations. However, somewhere between the Event.destroy and the Commentable.destroy, some other rails code destroys the record in the has_and_belongs_to_many table.
Any idea why this is happening and how to stop it?
You don't really have to override destroy on Commentable model, just add a before_destroy callback that return false to actually cancels the destroy call. For example:
class Commentable < ActiveRecord::Base
# ... some code ...
before_destroy { |record|
comments.destroy_all
self.deleted = true
self.save!
false
}
# ... some code ...
end
The same goes for Event model; just add a callback without overriding the destroy method itself.
More on the available callbacks is here.
Rails 5 doesn't halt callback chain if false is returned. We'll have to use throw(:abort) instead.
before_destroy :destroy_validation
def destroy_validation
if condition
errors.add(:base, "item cannot be destroyed because of the reason...")
throw(:abort)
end
end
Before Rails 5, returning false from any before_ callback in ActiveModel or ActiveModel::Validations, ActiveRecord & ActiveSupport resulted in halting of callback chain.
We can turn off this default behavior by changing this configuration to true. However then Rails shows deprecation warning when false is returned from callback.
The new Rails 5 application comes up with an initializer named callback_terminator.rb.
ActiveSupport.halt_callback_chains_on_return_false = false
By default the value is to set to false.
=> DEPRECATION WARNING: Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails. To explicitly halt the callback chain, please use `throw :abort` instead.
ActiveRecord::RecordNotSaved: Failed to save the record
This is a welcome change that will help prevent accidental halting of the callbacks.

Association won't save inside of before_validation

I'm trying to use the before_validation callback to adjust the number of child objects for a record, but for some reason, its not working the way I expect.
LineItem class:
before_validation :adjust_enrollment_count
def adjust_enrollment_count
if enrollments.size < quantity
(enrollments.size+1..quantity).each do |li|
self.enrollments.build(variant: self.variant)
end
#self.save
elsif enrollments.size > quantity
enrollments.delete_if do |e|
enrollments.size > quantity
end
end
end
What happens is that it creates the correct number of Enrollment objects as children to the LineItem, but the Variant gets set to nil (even though the LineItem has a variant defined).
Things I've tried:
Explicitly saving the line_item or the enrollment
"pry"ing into the callback and running the code manually (this actually worked the way
I expected!)
Verifying that "self" referred to the LineItem and not the closure
Is there something about the callback lifecycle that I'm missing? Is there a better way to adjust the number of Enrollment objects as the quantity changes on the LineItem?
Probably variant is not an accessible field of the Enrollment class. Try this way (also shortened)
def adjust_enrollment_count
while enrollments.size < quantity
self.enrollments.build(variant_id: self.variant) # note: variant_id
end
while enrollments.size > quantity
enrollments.pop # or .shift to delete from the head of the list
end
# don't save in a lifecycle callback, or you'll get in an awful loop
end
EDIT: a different take
def add_enrollment
enrollments.build(variant_id: variant)
end
def adjust_enrollment_count
enrollments.slice!(quantity, enrollments.size)
add_enrollment while enrollments.length < quantity
end
It turned out that the problem was something that I didn't have outlined in my question. I had defined the following:
class Enrollment < ActiveRecord::Base
belongs_to :line_item
attr_accessible :variant
attr_accessor :variant
end
I think the attr_accessor was creating an in-memory variable called variant that only lasted as long as the page load. I removed that and it seemed to solve the problem.

Resources