December 8, 2020
This blog is part of our Rails 6.1 series.
In Rails 6.1, Rails will enqueue a background job to destroy associated records
if dependent: :destroy_async
is setup.
Let's consider the following example.
class Team < ApplicationRecord
has_many :players, dependent: :destroy_async
end
class Player < ApplicationRecord
belongs_to :team
end
Now, if we call the destroy
method on an instance of class Team
Rails would
enqueue an asynchronous job to delete the associated players
records.
We can verify this asynchronous job with the following test case.
class TeamTest < ActiveSupport::TestCase
include ActiveJob::TestHelper
test "destroying a record destroys the associations using a background job" do
team = Team.create!(name: "Portugal", manager: "Fernando Santos")
player1 = Player.new(name: "Bernardo Silva")
player2 = Player.new(name: "Diogo Jota")
team.players << [player1, player2]
team.save!
team.destroy
assert_enqueued_jobs 1
assert_difference -> { Player.count }, -2 do
perform_enqueued_jobs
end
end
end
Finished in 0.232213s, 4.3064 runs/s, 8.6128 assertions/s.
1 runs, 2 assertions, 0 failures, 0 errors, 0 skips
Alternatively, this enqueue behavior can also be demonstrated in
rails console
.
irb(main):011:0> team.destroy
TRANSACTION (0.1ms) begin transaction
Player Load (0.6ms) SELECT "players".* FROM "players" WHERE "players"."team_id" = ? [["team_id", 6]]
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 4df07c2d-f55b-48c9-8c20-545b086adca2) to Async(active_record_destroy) with arguments: {:owner_model_name=>"Team", :owner_id=>6, :association_class=>"Player", :association_ids=>[1, 2], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 4df07c2d-f55b-48c9-8c20-545b086adca2) from Async(active_record_destroy) in 34.5ms
However, this behaviour is inconsistent and the destroy_async
option should
not be used when the association is backed by foreign key constraints in the
database.
Let us consider another example.
CASE: With a simple foreign key on the team_id
column in place.
irb(main):015:0> team.destroy
TRANSACTION (0.1ms) begin transaction
Player Load (0.1ms) SELECT "players".* FROM "players" WHERE "players"."team_id" = ? [["team_id", 7]]
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 69e51e5f-5b59-4095-92db-90aab73a7f65) to Async(default) with arguments: {:owner_model_name=>"Team", :owner_id=>7, :association_class=>"Player", :association_ids=>[1], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Team Destroy (0.9ms) DELETE FROM "teams" WHERE "teams"."id" = ? [["id", 7]]
TRANSACTION (1.1ms) rollback transaction
Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 69e51e5f-5b59-4095-92db-90aab73a7f65) from Async(default) enqueued at 2021-01-03T21:10:21Z with arguments: {:owner_model_name=>"Team", :owner_id=>7, :association_class=>"Player", :association_ids=>[1], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Traceback (most recent call last):
1: from (irb):15
ActiveRecord::InvalidForeignKey (SQLite3::ConstraintException: FOREIGN KEY constraint failed)
An exception is raised by Rails and the record is not destroyed.
CASE: With a cascading foreign key using on_delete: :cascade
Here, even though ActiveRecord::DestroyAssociationAsyncJob
would run to
successful completion, the associated players
records would already be deleted
inside the same transaction block destroying the team
record, and it would
skip any destroy callbacks like before_destroy
, after_destroy
or
after_commit on: :destroy
.
This makes using destroy_async
redundant in such a case.
Check out the pull request for more details.
If this blog was helpful, check out our full blog archive.