Rails - AciveRecord use :dependent => :destroy on condition - ruby-on-rails

What will be the best/DRY way to destroy all the dependents of an object based on a condition. ?
Ex:
class Worker < ActiveRecord::Base
has_many :jobs , :dependent => :destroy
has_many :coworkers , :dependent => :destroy
has_many :company_credit_cards, :dependent => :destroy
end
condition will be
on Destroy:
if self.is_fired?
#Destroy dependants records
else
# Do not Destroy records
end
Is There a Way to use Proc in the :dependent condition.
I have found the methods to destroy the dependents individually, but this is not DRY and flexible for further associations,
Note: I have made up the example.. not an actual logic

No. You should remove :dependent => :destroy and add after_destroy callback where you can write any logic you want.
class Worker < ActiveRecord::Base
has_many :jobs
has_many :coworkers
has_many :company_credit_cards
after_destroy :cleanup
private
def cleanup
if self.is_fired?
self.jobs.destroy_all
self.coworkers.destroy_all
self.company_credit_cards.destroy_all
end
end
end

Related

after_destroy not called for linked table

I have these models
class User < ActiveRecord::Base
has_many :user_functions, :dependent => :destroy
has_many :functions, :through => :user_functions
accepts_nested_attributes_for :functions, allow_destroy: true
Model of the linked table:
class UserFunction < ActiveRecord::Base
belongs_to :user, inverse_of: :user_functions
belongs_to :function, inverse_of: :user_functions
after_destroy :unplan_items
after_create :plan_items
and of course the model of function but this is like user...
Now when I do the following in my tests:
#user.functions = [#functions]
#user.save
expect(#user.planned_items.count).to eq(1)
#user.functions = []
#user.save
I notice the callback after_destroy isn't called. Why is this and how can I avoid this. There are certain steps that need to be done every time a UserFunction is destroyed...
I believe this has to do with: https://github.com/rails/rails/issues/7618 (I'm using rails 4.2.5 though). The after_create is working perfect though...
Currently rails uses :delete_all as default strategy of has_many_through. It only calls :destroy_all when we explicitly specify dependent: :destroy on the association.
The docs mention advice to use has_many :through if you need callbacks:
See the suggestion here: http://guides.rubyonrails.org/association_basics.html
You should use has_many :through if you need validations, callbacks,
or extra attributes on the join model.
So there currently is an inconsistency between after_create which does do the callback and after_destroy.
This is mentioned in these two issues posted on GitHub:
https://github.com/rails/rails/issues/7618
https://github.com/rails/rails/issues/27099
The fix for now is to explicitly put :dependent => :destroy on the :through part. This will make sure the callback are used.
class User < ActiveRecord::Base
has_many :user_functions
has_many :functions, :through => :user_functions, :dependent => :destroy
accepts_nested_attributes_for :functions, allow_destroy: true
For anyone reading this 2021+
Change This
has_many :object_tags, :as => :taggable, :dependent => :destroy
has_many :tags, :through => :object_tags
To This
has_many :object_tags
has_many :tags, :through => :object_tags, :dependent => :destroy

Howto use callbacks in a has_many through association?

I have a Task model associated to a Project model via has_many through and need to manipulate the data before delete/insert via the association.
Since "Automatic deletion of join models is direct, no destroy callbacks are triggered." i can not use callbacks for this.
In Task i need all project_ids to calculate a value for Project after Task is saved.
How can i disable delete or change delete to destroy on has_many through association?
What is best practise for this problem?
class Task
has_many :project_tasks
has_many :projects, :through => :project_tasks
class ProjectTask
belongs_to :project
belongs_to :task
class Project
has_many :project_tasks
has_many :tasks, :through => :project_tasks
Seems like i have to use associations callbacks before_add, after_add, before_remove or after_remove
class Task
has_many :project_tasks
has_many :projects, :through => :project_tasks,
:before_remove => :my_before_remove,
:after_remove => :my_after_remove
protected
def my_before_remove(obj)
...
end
def my_after_remove(obj)
...
end
end
This is what I did
in the model:
class Body < ActiveRecord::Base
has_many :hands, dependent: destroy
has_many :fingers, through: :hands, after_remove: :touch_self
end
in my Lib folder:
module ActiveRecord
class Base
private
def touch_self(obj)
obj.touch && self.touch
end
end
end
Updating joins model associations, Rails add and remove records on the collection. To remove the records, Rails use the delete method and this one will not call any destroy callback.
You can force Rails to call destroy instead delete when is removing records.
To do that, install the gem replace_with_destroy and pass the option replace_with_destroy: true to the has_many association.
class Task
has_many :project_tasks
has_many :projects, :through => :project_tasks,
replace_with_destroy: true
...
end
class ProjectTask
belongs_to :project
belongs_to :task
# any destroy callback in this model will be executed
#...
end
class Project
...
end
With this, you ensure Rails invoke all the destroy callbacks. This could be very useful if you are using paranoia.
It seems like adding dependent: :destroy to has_many :through relation will destroy the join model (instead of delete). This is because CollectionAssociation#delete internally refers to :dependent option to determine whether the passed records should be deleted or destroyed.
So in your case,
class Task
has_many :project_tasks
has_many :projects, :through => :project_tasks, :dependent => :destroy
end
should work.

ActiveRecord - has_many :through, :dependent => :destroy sql is not correct

I am trying to "counter cache" the number of posts in each tag. The after save callback is working but the after destroy is not. Looks like destroy sql is not correct.
class Post < ActiveRecord::Base
has_many :post_tags, :dependent => :destroy
has_many :tags, :through => :post_tags
end
class Tag < ActiveRecord::Base
has_many :post_tags, :dependent => :destroy
has_many :posts, :through => :post_tags
end
class PostTag < ActiveRecord::Base
self.table_name = :posts_tags
belongs_to :post
belongs_to :tag
after_save :update_tag_posts_count
after_destroy :update_tag_posts_count
def update_tag_posts_count
tag.posts_count = tag.posts.count
tag.save
end
end
The test fails
# #tag.posts_count == 10
Failure/Error: #tag.posts.first.destroy
ActiveRecord::StatementInvalid:
Mysql2::Error: Unknown column 'posts_tags.' in 'where clause': DELETE FROM `posts_tags` WHERE `posts_tags`.`` = NULL
The correct sql should be
DELETE FROM `posts_tags` WHERE `posts_tags`.`post_id` = {the post id}
I had the exact same issue, the fix for me was to add a primary key column to the join table (PostTag in your case).
It seems that rails needs a primary key for the :dependent => :destroy option to work.
May be because in models you used
#post.rb
has_many :post_tags, :dependent => :destroy
#tag.rb
has_many :post_tags, :dependent => :destroy
instead of
#post.rb
has_many :posts_tags, :dependent => :destroy
#tag.rb
has_many :posts_tags, :dependent => :destroy
?
I'd suggest separate increment/decrement functions and not using count + save. This should function properly, perform well, not trigger side-effects on Tag, and not mess up your count in race conditions.
class PostTag < ActiveRecord::Base
belongs_to :post
belongs_to :tag
after_create :increment_tag_posts_counter
after_destroy :decrement_tag_posts_counter
private
def increment_tag_posts_counter
Tag.increment_counter :posts_count, post_id
end
def decrement_tag_posts_counter
Tag.decrement_counter :posts_count, post_id
end
end

Optimizing rails query and associations

I have the following associations in my application:
# user.rb
has_many :posts, :dependent => :destroy
has_many :likes, :dependent => :destroy
# post.rb
belongs_to :user
has_many :likes, :dependent => :destroy
# like.rb
belongs_to :user
belongs_to :post
When I'm trying to access all the posts that a user liked I'm using the following loop
#user = User.find(params[:id])
#posts_user_likes = []
#user.likes.each do |like| # TODO optimize
#posts_user_likes << Post.find_by_id(like.post_id)
end
but that seems very inefficient.
What's the best way to improve my code, either with different association or different way of looping?
Add has_many :liked_posts, :through => :likes, :class_name => 'Post' to User and then call User.find(params[:id]).liked_posts.

Determine what ids were removed from array in Rails after_save callback?

I am trying to figure out how i can tell what has changed in an array in the after save callback. Here is an example of code i am using:
class User < ActiveRecord::Base
has_many :user_maps, :dependent => :destroy
has_many :subusers, :through => :user_maps, :dependent => :destroy
has_many :inverse_user_maps, :class_name => "UserMap", :foreign_key => "subuser_id"
has_one :parent, :through => :inverse_user_maps, :source => :user
after_save :remove_subusers
def remove_subusers
if self.subuser_ids_were != self.subuser_ids
leftover = self.subuser_ids_were - self.subuser_ids
leftover.each do |subuser|
subuser.destroy
end
end
end
end
class UserMap < ActiveRecord::Base
belongs_to :user
belongs_to :subuser, :class_name => "User"
end
I am removing the subusers with the after_save callback because i could not get the dependent destroy feature to work through user_maps. Does anyone have any ideas on a way to do this?
Thanks!
You can use the Dirty module accessors http://ar.rubyonrails.org/classes/ActiveRecord/Dirty.html as suggested in Determine what attributes were changed in Rails after_save callback?
In your case the handler you have for after_save will have access to subusers_change which is an array of two elements, first being the previous value and second being the new value.
although not strictly the answer to your question, I think you maybe able to get :dependent => :destroy working if you try the following...
class User < ActiveRecord::Base
has_many :user_maps, :dependent => :destroy
has_many :subusers, :through => :user_maps # removing the :dependent => :destroy option
end
class UserMap < ActiveRecord::Base
belongs_to :user
belongs_to :subuser, :class_name => "User", :dependent => :destroy # add it here
end
By moving the :dependent => :destroy option to the belongs_to association in the UserMap model you set up a cascading delete via the UserMap#destroy method. In other words, calling User#destroy will call UserMap#destroy for each UserMap record, which will in turn call sub_user.destroy for its sub_user record.
EDIT
Since the solution above didn't work, my next suggestion would be to add a callback to the user_maps association, however this comes with a warning that I will add after
class User < ActiveRecord::Base
has_many :user_maps, :dependent => :destroy, :before_remove => :remove_associated_subuser
def remove_associated_subuser(user_map)
user_map.subuser.destroy
end
end
WARNINGS
1) Using a before_remove callback will mean that the user_map.destroy function won't be called if there is an error with the callback
2) You will have to destroy your UserMap record using the method on the User class for example...
# this will fire the callback
u = User.first
u.user_maps.destroy(u.user_maps.first)
# this WONT fire the callback
UserMap.first.destroy
All things considered, this would make me nervous. I would first try modifying your code to make the associations a little less coupled to the same tables, so the :dependent => :destroy option can work, and if you can't do that, add a cascade delete constraint on to the database, at least then your associations will always be removed regardless of where / how you destroy it in your rails app.

Resources