All Articles

Rails allows specifying the maximum number of records that will be destroyed in a single background job.

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.