I have venues and venue_managers, with a HABTM relationship, and I am rolling my own counter cache via after_add and after_remove callbacks. Those callbacks on the venue_managers are getting added to the venue model via a mixin. I am using am mixin because there are other models that need this same logic.
If there are 2 venue_managers and I re-assign venue_managers to just have 1, with venue.venue_managers = a_user, then the after_remove callback gets triggered and all is well. If I have 2 venue_managers and I destroy the last one with venue.venue_managers.last.destroy, the after_remove callback is NOT triggered and the counter cache does not update, which is my problem.
class Venue < ActiveRecord::Base
include HABTMCounterCache
has_and_belongs_to_many :venue_managers, :class_name => 'User', :join_table => :venues_venue_managers
end
module HABTMCounterCache
extend ::ActiveSupport::Concern
module ClassMethods
def count_cache_associations
klass = self.name.downcase
self.send("after_add_for_#{klass}_managers") << lambda do |obj, manager|
obj.update_counter_cache(manager)
end
self.send("after_remove_for_#{klass}_managers") << lambda do |obj, manager|
obj.update_counter_cache(manager)
end
end
end
def update_counter_cache(noop)
klass = self.class.name.downcase
self.assign_attributes(:"#{klass}_managers_count" => self.send("#{klass}_managers").count)
end
end
Related
how to skip_callback while building has_many relationship objects in rails 5
Consider below case
class Customer
has_many :resources
end
class Resource
attr_accessor: :skip_callback
belongs_to :customer
after_commit :data_calculation, unless: :skip_callback
def data_calculation
# logic goes here
end
end
customer = Customer.new
customer.resources.build({name: 'abc'})
customer.save
I want skip callback of associated object.
Can we do this while building object?
found solution,
customer = Customer.new
customer.resources.build({name: 'abc', skip_callback: true})
customer.save
passing attr_accessor as params will set callbacks condition value also.
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
I have implemented a tagging system for the models Unit, Group and Event, and currently, each one have their own instance of the methods add_tags and self.tagged_with.
def add_tags(options=[])
transaction do
options.each do |tag|
self.tags << Tag.find_by(name: tag)
end
end
end
and
def self.tagged_with(tags=[])
units = Unit.all
tags.each do |tag|
units = units & Tag.find_by_name(tag).units
end
units
end
end
I want to move these into a module and include them in the model, but as you can see, the tagged_with method is not polymorphic, as I don't know how I would refer the parenting class (Unit, Group etc.) and called methods like "all" on them. Any advice?
Tag model:
Class Tag < ActiveRecord::Base
has_and_belongs_to_many: units, :join_table => :unit_taggings
has_and_belongs_to_many: groups, :join_table => :group_taggings
has_and_belongs_to_many: events, :join_table => :event_taggings
end
You could call self.class to get the current class, like this:
def self.tagged_with(tags=[])
klass = self.class
units = klass.all
tags.each do |tag|
units = units & Tag.find_by_name(tag).units
end
units
end
end
self.class should return Unit or any class, calling any method on a class object (self.class.tagged_with) is the same as Unit.tagged_with
I would recommend that you use Concerns, take a look here
EDIT Answer to your comment
Using concerns you could do something like this, each class have that methods you mentioned before, but you dont have to rewrite all that code on every class (or file):
# app/models/concerns/taggable.rb
module Taggable
extend ActiveSupport::Concern
module ClassMethods
def self.tagged_with(tags=[])
klass = self.class
units = klass.all
tags.each do |tag|
units = units & Tag.find_by_name(tag).units
end
units
end
end
end
end
# app/models/unit.rb
class Unit
include Taggable
...
end
# app/models/group.rb
class Group
include Taggable
...
end
# app/models/event.rb
class Event
include Taggable
...
end
I would do it like so:
#table: taggings, fields: tag_id, taggable type (string), taggable id
class Tagging
belongs_to :tag
belongs_to :taggable, :polymorphic => true
Now make a module in lib - let's call it "ActsAsTaggable"*
module ActsAsTaggable
def self.included(base)
base.extend(ClassMethods)
base.class_eval do
#common associations, callbacks, validations etc go here
has_many :taggings, :as => :taggable, :dependent => :destroy
has_many :tags, :through => :taggings
end
end
#instance methods can be defined in the normal way
#class methods go in here
module ClassMethods
end
end
Now you can do this in any class you want to make taggable
include ActsAsTaggable
there is already a gem (or plugin perhaps) called ActsAsTaggable, which basically works in this way. But it's nicer to see the explanation rather than just get told to use the gem.
EDIT: here's the code you need to set up the association at the Tag end: note the source option.
class Tag
has_many :taggings
has_many :taggables, :through => :taggings, :source => :taggable
I have an UnreadEntry model and am using an after_commit callback to send a notification to a pusher service. The problem is event fires just fine when adding records but when a delete_all is sent on the model, neither: after_commit, after_destroy are fired.
How can I add catch delete_all and add a callback to it?
class UnreadEntry < ActiveRecord::Base
belongs_to :user
belongs_to :feed
belongs_to :entry
after_commit :send_pusher_notification, if: PUSHER_ENABLED
validates_uniqueness_of :user_id, scope: :entry_id
def self.create_from_owners(user, entry)
create(user_id: user.id, feed_id: entry.feed_id, entry_id: entry.id, published: entry.published, entry_created_at: entry.created_at)
end
def self.sort_preference(sort)
if sort == 'ASC'
order("published ASC")
else
order("published DESC")
end
end
def send_pusher_notification(user = nil, from = 'UnreadEntry#callback')
if user.nil?
unread_count = UnreadEntry.where(user_id: self.user_id).count
else
unread_count = UnreadEntry.where(user_id: user.id).count
end
Pusher['rssapp'].trigger('unread_count', {
message: unread_count
})
end
end
Simple - don't use delete_all. delete_all and update_all are specifically designed to query the database directly, bypassing the ActiveRecord model logic - including validations and callbacks. If you want callbacks, call destroy on each model instance individually.
my_collection.each(&:destroy)
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