Mark Berry
Mark Berry

Reputation: 19032

Rails ActiveRecord 3.2: How to skip before_delete callback in child model?

I'm working in Rails 3.2.16. In my app, an account has_many users. An account must always have admin users, so you can't destroy them. This takes care of that:

class Account < ActiveRecord::Base
  has_many :users, :dependent => :destroy
end

class User < ActiveRecord::Base
  before_destroy :check_if_admin

  def check_if_admin
    false if self.is_admin
  end
end

However, when you destroy the whole account, the admins should be destroyed as well. Instead, when I call @account.destroy from the controller, the User#before_delete callback prevents the admin users from being destroyed.

I know I could call @account.delete to skip callbacks, but my understanding is that the :dependent => :destroy is itself a callback, so that would only delete the account, not the users.

Is there a way inside the callback to know where I came from, e.g.

def check_if_admin
  return if [I'm doing an Account dependent delete]
  false if self.is_admin
end

or do I have to manually delete the users before destroying the account?

Upvotes: 2

Views: 1849

Answers (1)

Mark Berry
Mark Berry

Reputation: 19032

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.

Upvotes: 3

Related Questions