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 thedestroy
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:
Option | Behavior |
---|---|
:destroy | Deletes associated records and triggers callbacks. |
:delete_all | Deletes associated records directly in SQL (bypasses callbacks). |
:nullify | Sets foreign keys to NULL instead of deleting records. |
:restrict_with_error | Prevents 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 historicalMember
records, losing valuable audit data.dependent: :nullify
→ Would leaveMember
records without aTeam
, 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.
: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 multipleTask
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.
Check out my one-person software studio: https://typeangle.com/ and reach out if you think I'm a good fit for your needs.