50

I have looked through the Ruby on Rails guides and I can't seem to figure out how to prevent someone from deleting a Parent record if it has Children. For example. If my database has CUSTOMERS and each customer can have multiple ORDERS, I want to prevent someone from deleting a customer if it has any orders in the database. They should only be able to delete a customer if it has no orders.

Is there a way when defining the association between models to enforce this behavior?

Rob
  • 792
  • 2
  • 7
  • 15

4 Answers4

117
class Customer < ActiveRecord::Base
  has_many :orders, :dependent => :restrict # raises ActiveRecord::DeleteRestrictionError

Edit: as of Rails 4.1, :restrict is not a valid option, and instead you should use either :restrict_with_error or :restrict_with_exception

Eg.:

class Customer < ActiveRecord::Base
  has_many :orders, :dependent => :restrict_with_error
joshua.paling
  • 13,762
  • 4
  • 45
  • 60
Mauro
  • 1,225
  • 2
  • 8
  • 9
  • 2
    Can the restriction message be customised? – Cristian Nicoleta Jan 27 '15 at 20:57
  • 2
    Can this answer please be updated to reflect changes in Rails 4.1 and above? "Removed support for deprecated option :restrict for :dependent in associations." http://edgeguides.rubyonrails.org/4_1_release_notes.html. The :dependent option must be one of [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception] – Marklar Mar 17 '15 at 03:24
  • hi thank you - what is the difference between restricting with errors vs exceptions? – BenKoshy Oct 12 '16 at 04:00
  • 1
    `restrict_with_error` adds a Rails error to the respective attribute, while `restrict_with_exception` raises the mentioned exception. See https://edgeguides.rubyonrails.org/association_basics.html#options-for-has-one-dependent – codener Feb 18 '19 at 12:08
53

You could do this in a callback:

class Customer < ActiveRecord::Base
  has_many :orders
  before_destroy :check_for_orders

  private

  def check_for_orders
    if orders.count > 0
      errors.add_to_base("cannot delete customer while orders exist")
      return false
    end
  end
end

EDIT

see this answer for a better way to do this.

Community
  • 1
  • 1
zetetic
  • 47,184
  • 10
  • 111
  • 119
  • 5
    This is the best way. It's the cleanest, and it's exactly where I would look for such a filter if I was working on your code. Returning "false" in the callback is what tells rails not to proceed with the action. – Jaime Bellmyer Oct 29 '10 at 17:22
  • 3
    You may want to consider using Errors#add(:base, msg) instead. – Jon Bringhurst Apr 04 '11 at 23:00
  • 2
    I've created a small ActiveRecord plugin for applications that need this functionality in various models: https://github.com/Florent2/prevent_destroy_if_any – Florent2 Aug 08 '11 at 01:48
  • 3
    This is NOT the best way. A simple `dependent: :restrict` is, as per @Mauro's answer. Though I suspect this answer was accepted before the introduction of dependent restrict. – Damien Roche Mar 12 '13 at 18:27
  • 3
    @Zenph Yup, :dependent => :restrict was added later, and is the better solution. – zetetic Mar 12 '13 at 19:36
  • 1
    @zetetic I think it would be beneficial for others if you updated your answer. Some visitors might just take the first snippet they find (which happens to be outdated and overcomplicated). I came here from Google myself. – Damien Roche Mar 12 '13 at 22:39
  • @zetetic on an unrelated note, I love the fact you're a long-term Railist :) Bring on Rails 4! – Damien Roche Mar 12 '13 at 22:46
  • @Zenph @zetetic `dependent: :restrict` will cause unhandled exception in default scaffold. This answer is currently more complete. The "better" answer needs to be augmented with a way of handling the exception at base controller level. Even then, the situation is not "exceptional". – nurettin Oct 04 '13 at 07:32
  • @nurettin now there is the dependent: :restrict_with_error option to handle the case you talked about 5 years ago. The accepted answer should be changed or updated. – tommyalvarez Oct 29 '18 at 15:13
0

Try using filters to hook in custom code during request processing.

Joseph Weissman
  • 5,697
  • 5
  • 46
  • 75
0

One possibility would be to avoid providing your users a link to deletion in this scenario.

link_to_unless !@customer.orders.empty?

Another way would be to handle this in your controller:

if !@customer.orders.empty?
  flash[:notice] = "Cannot delete a customer with orders"
  render :action => :some_action
end

Or, as Joe suggests, before_filters could work well here and would probably be a much more DRY way of doing this, especially if you want this type of behavior for more models than just Customer.

Samo
  • 8,202
  • 13
  • 58
  • 95