Restrict with Error in Rails

Restrict Destroying Dependent Rails Associations with Error

The dependent: :restrict_with_error option is a simple way to enforce data integrity in Rails apps. By preventing deletions of parent records with existing associations and providing helpful errors, it ensures historical data remains intact while guiding users on how to handle dependencies properly.

4 min read

When working with associations in Rails, managing referential integrity is quite important. One not-so-common but highly useful option is dependent: :restrict_with_error, which ensures that records with dependent associations cannot be deleted without first handling those dependencies properly.

In this post, we'll explore how dependent: :restrict_with_error works, why it's useful, and when to use it over other dependent options like :destroy, :nullify, and :delete_all. I recently discovered this option while working on a new feature, and wanted to share a summary of my understanding. I hope you find it helpful as well.

Let's consider the following has_many association where a Team model has many Member records, and a Member belongs to a Team.

class Team < ApplicationRecord
  has_many :members
end

class Member < ApplicationRecord
  belongs_to :team
end

Typically, with has_many association, you specify the dependent option, which controls what happens to the associated objects when the owner is destroyed. Most of the time, you'd use the dependent: :destroy option, which causes all the associated objects to also be destroyed, as the following test shows.

class Team < ApplicationRecord
  has_many :members, dependent: :destroy
end

class Member < ApplicationRecord
  belongs_to :team
end


require "test_helper"

class TeamTest < ActiveSupport::TestCase
  def setup
    @team = Team.create(name: "Engineering Team")
  end

  test "deleting team deletes associated members" do
    member_one = Member.create(name: "John Doe", team: @team)
    member_two = Member.create(name: "Jane Doe", team: @team)

    assert_difference "Member.count", -2 do
      @team.destroy
    end
  end
end

However, there are some times when you don't want to allow deletion of the parent records when there are one or more associated records referencing it. Allowing deletion can lead to unintended data inconsistencies.

For example, let's imagine that for some business constraints, we don't want to allow any members to exist without a team. Hence we'd like to prevent deletion of a team when it has one or more members. Users can delete a team only after all the members of that team have been destroyed.

Rails provides the dependent: :restrict_with_error option to handle this exact use-case. Let's give it a try.

class Team < ApplicationRecord
  has_many :members, dependent: :restrict_with_error
end

With this configuration, attempting to delete a Team that still has associated Member records will:

  • Prevent the deletion of the team.
  • Add an error to the team model.
  • Return false from the destroy method.
  • Keep the database record intact to maintain referential integrity.

This behavior ensures that historical data remains valid and prevents accidental data loss. The following test that shows this behavior.

require "test_helper"

class TeamTest < ActiveSupport::TestCase
  def setup
    @team = Team.create(name: "Engineering Team")
  end

  test "cannot delete team with members" do
    member_one = Member.create(name: "John Doe", team: @team)
    member_two = Member.create(name: "Jane Doe", team: @team)

    assert_not @team.destroy
    assert_equal 2, Member.count
    assert_includes @team.errors.full_messages, "Cannot delete record because dependent members exist"
  end

  test "can delete team without members" do
    assert @team.destroy
    assert_equal 0, Member.count
  end
end

How Does It Differ from Other Options?

ActiveRecord provides multiple dependent options, each with its own implications:

OptionBehavior
:destroyDeletes associated records and triggers callbacks.
:delete_allDeletes associated records directly in SQL (bypasses callbacks).
:nullifySets foreign keys to NULL instead of deleting records.
:restrict_with_errorPrevents deletion if associated records exist and adds an error.

When no option is given, the default behavior is to do nothing with the associated records when destroying a record.

Let's compare alternative approaches and why they wouldn't work as well in this scenario:

  • dependent: :destroy → Would delete historical Member records, losing valuable audit data.
  • dependent: :nullify → Would leave Member records without a Team, making it unclear why the members were created.
  • dependent: :delete_all → Would remove records in bulk without callbacks, potentially breaking data integrity.

Hence in this case the restrict_with_error option makes sense.

💡
Note: Rails also provides a :restrict_with_exception option which causes an ActiveRecord::DeleteRestrictionError exception to be raised if there are any associated records. However, I prefer restrict_with_error since it adds an error on the model.

Why and When to Use restrict_with_error?

  • To Ensure Data Integrity: A Project may be referenced by multiple Task records. Deleting the project would leave historical records without context, making data incomplete or meaningless.
  • To Provide User Feedback: When an attempt is made to delete a record with associated entries, Rails adds a validation error, which can be displayed to the user. This prevents silent failures and informs the user about necessary actions.
  • To Maintain an Audit Trail: For business applications that track project management, task assignments, or work logs, retaining historical references is critical for audits and analytics.

What's the takeaway? When designing your Rails models, consider the implications of different dependent options and choose the one that best fits your application's needs. In cases where data consistency is important, :restrict_with_error is quite often the best choice.


That's a wrap. I hope you found this article helpful and you learned something new.

As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.

If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.

👉
Building web applications with Rails and Hotwire is something I really enjoy. If you're looking for some help with your Ruby on Rails application — whether you're starting a new project or maintaining a legacy codebase — I can help.

Check out my one-person software studio: https://typeangle.com/ and reach out if you think I'm a good fit for your needs.