With the help of this answer, I found this solution. The idea is to temporarily turn off the specific child callback while destroying the parent. Note that I had to add the :prepend => :true
option to get my custom callback re-added to the front of the chain.
class Account < ActiveRecord::Base
before_destroy :disable_user_check_if_admin
before_destroy :enable_user_check_if_admin
has_many :users, :dependent => :destroy
def disable_user_check_if_admin
User.skip_callback(:destroy, :before, :check_if_admin)
end
def enable_user_check_if_admin
User.set_callback(:destroy, :before, :check_if_admin), :prepend => :true
end
end
class User < ActiveRecord::Base
before_destroy :check_if_admin
has_many :contacts, :dependent => :restrict
def check_if_admin
false if self.is_admin
end
end
Without :prepend => :true
, I ran into trouble because my User model also has_many :contacts, :dependent => :restrict
. The problem is that, skip_callback
actually deletes the callback and set_callback
re-adds the callback at the end of the callback chain. Using :prepend => :true
, I was able to insert my custom before_destroy :check_if_admin
callback at the front of the chain. See docs and source code here.
Alternative solution (not in use)
While I was fighting the callback sequence, I tried a different solution that leaves the callbacks intact. Borrowing from this answer, I used an accessor on Account to let me check when it is being deleted:
class Account < ActiveRecord::Base
before_destroy :disable_user_check_if_admin
before_destroy :enable_user_check_if_admin
has_many :users, :dependent => :destroy
attr_accessor :destroying
def disable_user_check_if_admin
self.destroying = true
end
def enable_user_check_if_admin
self.destroying = false
end
end
class User < ActiveRecord::Base
before_destroy :check_if_admin
has_many :contacts, :dependent => :restrict
def check_if_admin
return if self.account.destroying
false if self.is_admin
end
end
This did work but it doesn't "smell" right to be setting and checking a flag like this, so I went back to the skip/set callback using :prepend => :true
.