after_touch callback is fired multiple times - ruby-on-rails

I have a hierarchy of models, for which I'm trying to cascade a :touch event.
class Category < ActiveRecord::Base
has_many :posts
after_touch :do_stuff
def do_stuff
# do stuff...
end
end
class Post < ActiveRecord::Base
belongs_to :category, :touch => true
has_many :comments, :dependent => :destroy
end
class Comment < ActiveRecord::Base
belongs_to :post, :touch => true
end
I have a form for Post which creates a new Comment through nesteed_attributes. When this event occurs the after_touch method on the Category class is fired 4 times in quick succession (milliseconds apart) and I'm at a bit of a loss to understand why.
In my minds-eye the callback should only be fired once for the transaction? I also notice that in events such as destroying a post the callback is fired for the post, and each of the comments which is destroyed, resulting in many-many calls.
Is this normal behaviour? Is it expected? Is there a way around this? Is this a bug in Rails?

class Category < ActiveRecord::Base
has_many :posts
after_touch :do_stuff
def do_stuff
# do stuff...
end
end
class Post < ActiveRecord::Base
belongs_to :category
has_many :comments, :dependent => :destroy
end
class Comment < ActiveRecord::Base
belongs_to :post
end
posts_controller
def update
...
if #post.update
#post.category.touch
else
...
end
end

Seems that this is 'by design' within Rails at the moment. There also doesn't seem to be any immediate plan to change this, so for the moment if you're planning on using :touch and only require a single callback, you need to look at another option.
https://github.com/rails/rails/issues/8759

Related

Get Changes to ActiveRecord association

I have something like this:
class Post < ActiveRecord::Base
has_many :tag_members
has_many :tags, through: :tag_member
end
class Tag < ActiveRecord::Base
has_many :tag_members
has_many :posts, through: :tag_member
end
class TagMember < ActiveRecord::Base
belongs_to :tag
belongs_to :image
end
I want to track the edits on the post object. The easiest way to do this appears to be something like this:
class Post < ActiveRecord::Base
before_update :save_edits
def save_edits
# Assuming save_edit takes in a hash and persists it somehow
save_edit(self.changes)
end
end
However, from testing I've done, adding a new Tag to the has_many association on a Post does not run the before_update callback, and does not store anything in the hash returned by .changes.
What is the best way to track these types of edit as well? Should I simply overload the .tags= method to do my own storage, or is there a better way?
You could do something like this:
class TagMember < ActiveRecord::Base
after_save { |t| t.post.save }
# ^^^^
belongs_to :tag
belongs_to :image
belongs_to :post
# ^^^^
end

Rails: avoid recursive destroy callbacks

I have two models:
class Car < ActiveRecord::Base
has_many :adverts, :dependent => :destroy
end
class Advert < ActiveRecord::Base
belongs_to :car
# Destroy the car, if there is no more adverts left
after_destroy do
self.car.destroy unless self.car.adverts.exists?
end
end
Now this works well when calling advert.destroy, but when calling car.destroy, things end up in a recursive loop!
PS. rails 4.1.8

rails counter cache while destroy

For example, I have three model user, question and answer, and the relationship between them are:
class User < ActiveRecord::Base
has_many :answers
has_many :questions
end
class Question < ActiveRecord::Base
has_many :answers, :dependent => :destroy
belongs_to :user, :counter_cache => true
end
class Answer < ActiveRecord::Base
belongs_to :user, :counter_cache => true
belongs_to :question, :counter_cache => true
end
Then when i want to destroy a question(with 1000 answers), these will happen:
The answers will destroy one by one and will update the counter in the user model, even the counter in the question i want to destroy, and which will take a long time to do the counter update.
my question is how to make it faster?
I got my own solution like this:
step 1:
remove the dependent destroy, which will call for counter update before destroy itself.
step 2:
add my own before_destroy, like this
before_destroy :delete_dependents
and use delete_all function to delete without call any before_destroy, then call reset_counters function to reset the counter in User model.
full codes of class question:
class Question < ActiveRecord::Base
has_many :answers
has_many :answer_users, :through => :answers, :source => :user, :uniq => true
belongs_to :user, :counter_cache => true
before_destroy :delete_dependents
private
def delete_dependents
answer_user_ids = self.answer_user_ids # eager loading for store the answer users id
Answer.delete_all(:question_id => self.id)
answer_user_ids.each do |u_id|
User.reset_counters u_id, :answers
end
end
end
PS: If there are too many counters need to be reset, you might need a background job to work it out.

rails - how to pass changes back to parent model

I need to pass updated paramaters to back to a parent model when saving a series of its children.
For example if a save a bunch of employees to each task through a project, I need to let the project know the title of some of its tasks have changed, then I need to collect all the titles that changed and process them in the ProjectObserver. Is this possible?
I realize there might not be a way to make this work the way I'm trying. If not I'm happy to hear suggestions about how I might be able to get around this.
Here is what I have tried without any success:
class Employee < ActiveRecord::Base
has_many :employee_tasks
has_many :tasks, :through => :employee_tasks
accepts_nested_attributes_For :employee_tasks
accepts_nested_attributes_For :tasks
end
class Project < ActiveRecord::Base
attr_accessor :changed_employees
has_many :tasks
end
class Task < ActiveRecord::Base
has_many :employee_tasks
has_many :employees, :through => :employee_tasks
belongs_to :project
accepts_nested_attributes_For :employee_tasks
end
class EmployeeTask < ActiveRecord::Base
#this is what I want to accomplish
before_save do
if self.employee_id_changed
self.task.project.changed_employees ||= []
self.task.project.changed_employees << self.employee_id_changed
end
end
belongs_to :task
belongs_to :employee
end
class ProjectObserver < ActiveRecord::Observer
observe :project
def after_save(project)
puts project.changed_employees
# should print out the changed attributes loaded from EmployeeTask
#send a single email with all the updated titles (not one email for each change)
end
end
Sounds like you need to use the after_save method described here http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html

How to delete nested objects in Rails3?

How can I delete nested objects in a form? I found out that I need to add :allow_destroy in the parent model at the accepts_nested_attributes_for directive.
Further, I want to restrict the deletion. A nested object only should be deleted, if the parent object is the only one that retains the association.
Example:
class Internship < ActiveRecord::Base
belongs_to :company
accepts_nested_attributes_for :company, allow_destroy => true
end
class Company < ActiveRecord::Base
has_many :internships
end
Explanation: A company can host many internships. Therefore, I do not want to delete the company record as long as there is at least one other internship associated with it.
You could use dependent => :destroy
class Internship < ActiveRecord::Base
belongs_to :company
accepts_nested_attributes_for :company, allow_destroy => true
end
class Company < ActiveRecord::Base
has_many :internships, :dependent => :destroy
end
If you return false in a before_destroy filter, then the destroy action will be blocked. So we can check to see if there are any internships associated to the company, and block it if so. This is done in the company model.
class Company < ActiveRecord::Base
has_many :internships
before_destroy :ensure_no_internships
private
def ensure_no_internships
return false if self.internships.count > 0
end
end

Resources