Rails 6
had added the :destroy_async
option to delete the associated records in a background job whenever the foreign_key
constraint would be disabled.
But what if the associated records are too large? In this case, by default, the single background job will delete all of them and that can be time-consuming. To handle this, and make it fast, Rails now allows adding the active record configuration to specify the maximum number of records that will be destroyed in a single background job and enqueue additional jobs when the number of records exceeds that limit.
Example:
Suppose we have a Product
model
and a Review
model.
# product.rb
class Product < ApplicationRecord
has_many :reviews, dependent: :destroy_async
end
# review.rb
class Review < ApplicationRecord
belongs_to :product
end
Notice that we have added the :destroy_async
option to delete review
records in a background job.
Before
=> Product.find(1).destroy
=> Performing ActiveRecord::DestroyAssociationAsyncJob (Job ID: 880513a7-f6c4-4a35-8d33-6d69a737031e) from Async(default) enqueued at 2022-06-03T11:15:13Z with arguments: {:owner_model_name=>"Product", :owner_id=>1, :association_class=>"Review", :association_ids=>[1, 2, 3], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Product Load (1.6ms) SELECT "products".* FROM "products" WHERE "products"."id" = $1 LIMIT $2 [["id", 1], ["LIMIT", 1]]
Review Load (1.7ms) SELECT "reviews".* FROM "reviews" WHERE "reviews"."id" IN ($1, $2, $3) ORDER BY "reviews"."id" ASC LIMIT $4 [["id", 1], ["id", 2], ["id", 3], ["LIMIT", 1000]]
TRANSACTION (1.2ms) BEGIN
Review Destroy (1.4ms) DELETE FROM "reviews" WHERE "reviews"."id" = $1 [["id", 1]]
TRANSACTION (1.7ms) COMMIT
TRANSACTION (1.1ms) BEGIN
Review Destroy (1.3ms) DELETE FROM "reviews" WHERE "reviews"."id" = $1 [["id", 2]]
TRANSACTION (1.6ms) COMMIT
TRANSACTION (1.1ms) BEGIN
Review Destroy (1.2ms) DELETE FROM "reviews" WHERE "reviews"."id" = $1 [["id", 3]]
TRANSACTION (1.6ms) COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 880513a7-f6c4-4a35-8d33-6d69a737031e) from Async(default) in 51.36ms
As it can be seen all the reviews get deleted in a single async job with ID 880513a7-f6c4-4a35-8d33-6d69a737031e
.
After
Now, we can add the configuration to specify the maximum number of records that will be destroyed in a single background job.
/config/environments/development.rb
config.active_record.destroy_association_async_batch_size = 10
Now, if we try to delete a Product with 50 reviews,
Rails will enqueue 5 DestroyAssociationAsyncJobs
to delete all of them.
=> Product.find(2).reviews.count
=> 50
=> Product.find(2).destroy
=> Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: c141dbf1-124a-4659-89cd-15e89ddec6fe) to Async(default) with arguments: {:owner_model_name=>"Product", :owner_id=>2, :association_class=>"Review", :association_ids=>[4, 5, 6, 7, 8, 9, 10, 11, 12, 13], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 9e3d67ef-aebd-4e56-b6d1-d6bc520d8b74) to Async(default) with arguments: {:owner_model_name=>"Product", :owner_id=>2, :association_class=>"Review", :association_ids=>[14, 15, 16, 17, 18, 19, 20, 21, 22, 23], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: a5910f83-806d-467e-a9cf-0e40701d91aa) to Async(default) with arguments: {:owner_model_name=>"Product", :owner_id=>2, :association_class=>"Review", :association_ids=>[24, 25, 26, 27, 28, 29, 30, 31, 32, 33], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: 1588b961-b531-4ed7-acaa-8cbc71a98103) to Async(default) with arguments: {:owner_model_name=>"Product", :owner_id=>2, :association_class=>"Review", :association_ids=>[34, 35, 36, 37, 38, 39, 40, 41, 42, 43], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Enqueued ActiveRecord::DestroyAssociationAsyncJob (Job ID: b42848f7-e958-4b47-8670-e0cde6fc0cd9) to Async(default) with arguments: {:owner_model_name=>"Product", :owner_id=>2, :association_class=>"Review", :association_ids=>[44, 45, 46, 47, 48, 49, 50, 51, 52, 53], :association_primary_key_column=>:id, :ensuring_owner_was_method=>nil}
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 1588b961-b531-4ed7-acaa-8cbc71a98103) from Async(default) in 119.36ms
TRANSACTION (2.0ms) COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: b42848f7-e958-4b47-8670-e0cde6fc0cd9) from Async(default) in 120.38ms
TRANSACTION (2.3ms) COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: a5910f83-806d-467e-a9cf-0e40701d91aa) from Async(default) in 122.97ms
TRANSACTION (1.6ms) COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: c141dbf1-124a-4659-89cd-15e89ddec6fe) from Async(default) in 123.75ms
TRANSACTION (1.3ms) COMMIT
Performed ActiveRecord::DestroyAssociationAsyncJob (Job ID: 9e3d67ef-aebd-4e56-b6d1-d6bc520d8b74) from Async(default) in 166.56ms
Note: The enhancement is yet to be released in the official Rails version
Check out the PR for more details.