Rails ActiveRecord CounterCache and Callbacks - ruby-on-rails

Does counter_cache increments and decrements fire active_record callbacks ?
User << AR
has_many :cups
after_update :do_something
def do_something
"Will I be called when number of cups updated ?"
end
end
Cup << AR
belongs_to :user, counter_cache: true
end
In the above code will the function do_something be called when a new cup is added and it belongs to a user, update will be called on that user to update the cups_count, but from what I have tried it seems like the counter_cache updates don't fire callbacks, may be because they are themselves inside callbacks ?
Thanks

From the source for counter cache, it seems that ActiveRecord is doing a direct database update, which will skip callbacks.
update_all(updates.join(', '), primary_key => id )
According to the documentation for update_all, it does skip callbacks.

As #davogones mentions using callbacks is out, but you can still do something similar by overriding the update_counters method in your parent object.
In my case I needed to do something if the counter_cache count exceeded a certain value:
class Cups < ApplicationRecord
belongs_to :user, :counter_cache => true
end
class User < ApplicationRecord
has_many :cups
# This will be called every time there is a counter_cache update, + or -
def self.update_counters(id, counters)
user = User.find(id)
if user.cups_count + counters['cups_count'] >= some_value
user.do_something!
end
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.

Related

Paper Trail: create a version on parent whenever associated model changes?

I'm working on a Rails app where I need to show the audit trail on a Record, which has_many Data. I have paper_trail on my Record, and associated Datum models, and it is saving versions of them just fine.
However, what I need is for one version for Record to be created whenever one or more associated Data are changed. Currently, it creates versions on each Datum that changes, but it only creates a version of the Record if the Record's attributes change; it's not doing it when the associated Data change.
I tried putting touch_with_version in Record's after_touch callback, like so:
class Record < ActiveRecord::Base
has_many :data
has_paper_trail
after_touch do |record|
puts 'touched record'
record.touch_with_version
end
end
and
class Datum < ActiveRecord::Base
belongs_to :record, :touch => true
has_paper_trail
end
The after_touch callback fires, but unfortunately it creates a new version for each Datum, so when a Record is created it already has like 10 versions, one for each Datum.
Is there a way to tell in the callbacks if a version has been created, so I don't create multiples? Like check in one of the Record callbacks and if Datum has already triggered a version, don't do any more?
Thanks!
This works for me.
class Place < ActiveRecord::Base
has_paper_trail
before_update :check_update
def check_update
return if changed_notably?
tracking_has_many_associations = [ ... ]
tracking_has_has_one_associations = [ ... ]
tracking_has_many_associations.each do |a|
send(a).each do |r|
if r.send(:changed_notably?) || r.marked_for_destruction?
self.touch
return
end
end
end
tracking_has_one_associations.each do |a|
r = send(a)
if r.send(:changed_notably?) || r.marked_for_destruction?
self.touch
return
end
end
end
end

Save before initial save of an object

When a conversation is created, I want that conversation to have its creator automatically following it:
class Conversation < ActiveRecord::Base
belongs_to :user
has_many :followers
has_many :users, through: :followers
alias_method :user, :creator
before_create { add_follower(self.creator) }
def add_follower(user)
unless self.followers.exists?(user_id: user.id)
self.transaction do
self.update_attributes(follower_count: follower_count + 1)
self.followers.create(user_id: user.id)
end
end
end
end
However, when a user attempts to create a conversation I get a stack level too deep
. I'm creating an infinite loop, and I think this is because the before_create callback is being triggered by the self.update_attributes call.
So how should I efficiently update attributes before creation to stop this loop happening?
Option 1 (preferred)
Rename your column follower_count to followers_count and add:
class Follower
belongs_to :user, counter_cache: true
# you can avoid renaming the column with "counter_cache: :follower_count"
# rest of your code
end
Rails will handle updating followers_count for you.
Then change your add_follower method to:
def add_follower(user)
return if followers.exists?(user_id: user.id)
followers.build(user_id: user.id)
end
Option 2
If you don't want to use counter_cache, use update_column(:follower_count, follower_count + 1). update_column does not trigger any validations or callbacks.
Option 3
Finally you don't need to save anything at this point, just update the values and they will be saved when callback finishes:
def add_follower(user)
return if followers.exists?(user_id: user.id)
followers.build(user_id: user.id)
self.follower_count = follower_count + 1
end

Is there any before_update hook for an activerecord object if the object in the child relation is saved and touch is set?

I have
class Users < ActiveRecord::Base
belongs_to :team, touch: true
end
class Team < ActiveRecord::Base
has_many :users
before_save :update_popularity
before_update :update_popularity
def popularity
#secret algorythm
end
private
def update_popularity
# This is not called
end
end
User.first.name = 'John'
When I save a user I would like to update the team popularity as well. However the before_filter doesn't seem to be invoked?
Is there any proper way for doing this?
Try this before_update :update_popularity
Update:
After reviewing the API doc of touch method here, they say:
Please note that no validation is performed and only the after_touch callback is
executed.

Detect changes on existing ActiveRecord association

I am writing an ActiveRecord extension that needs to know when an association is modified. I know that generally I can use the :after_add and :after_remove callbacks but what if the association was already declared?
You could simply overwrite the setter for the association. That would also give you more freedom to find out about the changes, e.g. have the assoc object before and after the change E.g.
class User < ActiveRecord::Base
has_many :articles
def articles= new_array
old_array = self.articles
super new_array
# here you also could compare both arrays to find out about what changed
# e.g. old_array - new_array would yield articles which have been removed
# or new_array - old_array would give you the articles added
end
end
This also works with mass-assignment.
As you say, you can use after_add and after_remove callbacks. Additionally set after_commit filter for association models and notify "parent" about change.
class User < ActiveRecord::Base
has_many :articles, :after_add => :read, :after_remove => :read
def read(article)
# ;-)
end
end
class Article < ActiveRecord::Base
belongs_to :user
after_commit { user.read(self) }
end

How can I invoke the after_save callback when using 'counter_cache'?

I have a model that has counter_cache enabled for an association:
class Post
belongs_to :author, :counter_cache => true
end
class Author
has_many :posts
end
I am also using a cache fragment for each 'author' and I want to expire that cache whenever #author.posts_count is updated since that value is showing in the UI. The problem is that the internals of counter_cache (increment_counter and decrement_counter) don't appear to invoke the callbacks on Author, so there's no way for me to know when it happens except to expire the cache from within a Post observer (or cache sweeper) which just doesn't seem as clean.
Any ideas?
I had a similar requirement to do something on a counter update, in my case I needed to do something if the counter_cache count exceeded a certain value, my solution was to override the update_counters method like so:
class Post < ApplicationRecord
belongs_to :author, :counter_cache => true
end
class Author < ApplicationRecord
has_many :posts
def self.update_counters(id, counters)
author = Author.find(id)
author.do_something! if author.posts_count + counters['posts_count'] >= some_value
super(id, counters) # continue on with the normal update_counters flow.
end
end
See update_counters documentation for more info.
I couldn't get it to work either. In the end, I gave up and wrote my own cache_counter-like method and call it from the after_save callback.
I ended up keeping the cache_counter as it was, but then forcing the cache expiry through the Post's after_create callback, like this:
class Post
belongs_to :author, :counter_cache => true
after_create :force_author_cache_expiry
def force_author_cache_expiry
author.force_cache_expiry!
end
end
class Author
has_many :posts
def force_cache_expiry!
notify :force_expire_cache
end
end
then force_expire_cache(author) is a method in my AuthorSweeper class that expires the cache fragment.
Well, I was having the same problem and ended up in your post, but I discovered that, since the "after_" and "before_" callbacks are public methods, you can do the following:
class Author < ActiveRecord::Base
has_many :posts
Post.after_create do
# Do whatever you want, but...
self.class == Post # Beware of this
end
end
I don't know how much standard is to do this, but the methods are public, so I guess is ok.
If you want to keep cache and models separated you can use Sweepers.
I also have requirement to watch counter's change. after digging rails source code, counter_column is changed via direct SQL update. In other words, it will not trigger any callback(in your case, it will not trigger any callback in Author model when Post update).
from rails source code, counter_column was also changed by after_update callback.
My approach is give rails's way up, update counter_column by myself:
class Post
belongs_to :author
after_update :update_author_posts_counter
def update_author_posts_counter
# need to update for both previous author and new author
# find_by will not raise exception if there isn't any record
author_was = Author.find_by(id: author_id_was)
if author_was
author_was.update_posts_count!
end
if author
author.update_posts_count!
end
end
end
class Author
has_many :posts
after_update :expires_cache, if: :posts_count_changed?
def expires_cache
# do whatever you want
end
def update_posts_count!
update(posts_count: posts.count)
end
end

Resources