mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
ActiveRecord::Relation#destroy_all perform its work in batches
💇♀️ Fix specs Update activerecord/lib/active_record/relation.rb Co-authored-by: Eugene Kenny <elkenny@gmail.com> Update activerecord/lib/active_record/relation.rb Co-authored-by: Viktar Basharymau <6alliapumob@gmail.com> Use class attribute instead of mattr_accessor Add destroy all test case around value returned add Rails.application.config.active_record.destroy_all_in_batches too new defaults framework Reset Active Record Relation Document ActiveRecord::Relation#destroy_all Add changelog entry and update docs 💇♀️ Update method signature and argument docs Apply suggestions from code review Co-authored-by: Viktar Basharymau <6alliapumob@gmail.com> Co-authored-by: Alberto Almagro <albertoalmagro@gmail.com>
This commit is contained in:
parent
306449b757
commit
4e25cedf7f
9 changed files with 111 additions and 11 deletions
|
@ -1,3 +1,24 @@
|
||||||
|
* Relation#destroy_all perform its work in batches
|
||||||
|
|
||||||
|
Since destroy_all actually loads the entire relation and then iteratively destroys the records one by one,
|
||||||
|
you can blow your memory gasket very easily. So let's do the right thing by default
|
||||||
|
and do this work in batches of 100 by default and allow you to specify
|
||||||
|
the batch size like so: #destroy_all(batch_size: 100).
|
||||||
|
|
||||||
|
Apps upgrading to 7.0 will get a deprecation warning. As of Rails 7.1, destroy_all will no longer
|
||||||
|
return the collection of objects that were destroyed.
|
||||||
|
|
||||||
|
To transition to the new behaviour set the following in an initializer:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
config.active_record.destroy_all_in_batches = true
|
||||||
|
```
|
||||||
|
|
||||||
|
The option is on by default for newly generated Rails apps. Can be set in
|
||||||
|
an initializer to prevent differences across environments.
|
||||||
|
|
||||||
|
*Genadi Samokovarov*, *Roberto Miranda*
|
||||||
|
|
||||||
* Adds support for `if_not_exists` to `add_foreign_key` and `if_exists` to `remove_foreign_key`.
|
* Adds support for `if_not_exists` to `add_foreign_key` and `if_exists` to `remove_foreign_key`.
|
||||||
|
|
||||||
Applications can set their migrations to ignore exceptions raised when adding a foreign key
|
Applications can set their migrations to ignore exceptions raised when adding a foreign key
|
||||||
|
|
|
@ -137,7 +137,7 @@ module ActiveRecord
|
||||||
case method
|
case method
|
||||||
when :destroy
|
when :destroy
|
||||||
if scope.klass.primary_key
|
if scope.klass.primary_key
|
||||||
count = scope.destroy_all.count(&:destroyed?)
|
count = scope.each(&:destroy).count(&:destroyed?)
|
||||||
else
|
else
|
||||||
scope.each(&:_run_destroy_callbacks)
|
scope.each(&:_run_destroy_callbacks)
|
||||||
count = scope.delete_all
|
count = scope.delete_all
|
||||||
|
|
|
@ -77,6 +77,8 @@ module ActiveRecord
|
||||||
|
|
||||||
class_attribute :default_shard, instance_writer: false
|
class_attribute :default_shard, instance_writer: false
|
||||||
|
|
||||||
|
class_attribute :destroy_all_in_batches, instance_accessor: false, default: false
|
||||||
|
|
||||||
def self.application_record_class? # :nodoc:
|
def self.application_record_class? # :nodoc:
|
||||||
if ActiveRecord.application_record_class
|
if ActiveRecord.application_record_class
|
||||||
self == ActiveRecord.application_record_class
|
self == ActiveRecord.application_record_class
|
||||||
|
|
|
@ -560,8 +560,9 @@ module ActiveRecord
|
||||||
# Destroys the records by instantiating each
|
# Destroys the records by instantiating each
|
||||||
# record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method.
|
# record and calling its {#destroy}[rdoc-ref:Persistence#destroy] method.
|
||||||
# Each object's callbacks are executed (including <tt>:dependent</tt> association options).
|
# Each object's callbacks are executed (including <tt>:dependent</tt> association options).
|
||||||
# Returns the collection of objects that were destroyed; each will be frozen, to
|
# Returns the collection of objects that were destroyed if
|
||||||
# reflect that no changes should be made (since they can't be persisted).
|
# +config.active_record.destroy_all_in_batches+ is set to +false+. Each
|
||||||
|
# will be frozen, to reflect that no changes should be made (since they can't be persisted).
|
||||||
#
|
#
|
||||||
# Note: Instantiation, callback execution, and deletion of each
|
# Note: Instantiation, callback execution, and deletion of each
|
||||||
# record can be time consuming when you're removing many records at
|
# record can be time consuming when you're removing many records at
|
||||||
|
@ -573,9 +574,41 @@ module ActiveRecord
|
||||||
# ==== Examples
|
# ==== Examples
|
||||||
#
|
#
|
||||||
# Person.where(age: 0..18).destroy_all
|
# Person.where(age: 0..18).destroy_all
|
||||||
def destroy_all
|
#
|
||||||
|
# If +config.active_record.destroy_all_in_batches+ is set to +true+, it will ensure
|
||||||
|
# to perform the record's deletion in batches
|
||||||
|
# and destroy_all won't longer return the collection of the deleted records
|
||||||
|
#
|
||||||
|
# ==== Options
|
||||||
|
# * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value.
|
||||||
|
# * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value.
|
||||||
|
# * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000.
|
||||||
|
# * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when
|
||||||
|
# an order is present in the relation.
|
||||||
|
# * <tt>:order</tt> - Specifies the primary key order (can be :asc or :desc). Defaults to :asc.
|
||||||
|
#
|
||||||
|
# NOTE: These arguments are honoured only if +config.active_record.destroy_all_in_batches+ is set to +true+.
|
||||||
|
#
|
||||||
|
# ==== Examples
|
||||||
|
#
|
||||||
|
# # Let's process from record 10_000 on, in batches of 2000.
|
||||||
|
# Person.destroy_all(start: 10_000, batch_size: 2000)
|
||||||
|
#
|
||||||
|
def destroy_all(start: nil, finish: nil, batch_size: 1000, error_on_ignore: nil, order: :asc)
|
||||||
|
if ActiveRecord::Base.destroy_all_in_batches
|
||||||
|
batch_options = { of: batch_size, start: start, finish: finish, error_on_ignore: error_on_ignore, order: order }
|
||||||
|
in_batches(**batch_options).each_record(&:destroy).tap { reset }
|
||||||
|
else
|
||||||
|
ActiveSupport::Deprecation.warn(<<~MSG.squish)
|
||||||
|
As of Rails 7.1, destroy_all will no longer return the collection
|
||||||
|
of objects that were destroyed.
|
||||||
|
To transition to the new behaviour set the following in an
|
||||||
|
initializer:
|
||||||
|
Rails.application.config.active_record.destroy_all_in_batches = true
|
||||||
|
MSG
|
||||||
records.each(&:destroy).tap { reset }
|
records.each(&:destroy).tap { reset }
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Deletes the records without instantiating the records
|
# Deletes the records without instantiating the records
|
||||||
# first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy]
|
# first, and hence not calling the {#destroy}[rdoc-ref:Persistence#destroy]
|
||||||
|
|
|
@ -29,6 +29,9 @@ ARTest.connect
|
||||||
# Quote "type" if it's a reserved word for the current connection.
|
# Quote "type" if it's a reserved word for the current connection.
|
||||||
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type")
|
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type")
|
||||||
|
|
||||||
|
# FIXME: Remove this in Rails 7.1 when it's no longer needed.
|
||||||
|
ActiveRecord::Base.destroy_all_in_batches = true
|
||||||
|
|
||||||
def current_adapter?(*types)
|
def current_adapter?(*types)
|
||||||
types.any? do |type|
|
types.any? do |type|
|
||||||
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
|
ActiveRecord::ConnectionAdapters.const_defined?(type) &&
|
||||||
|
|
|
@ -12,18 +12,37 @@ class DeleteAllTest < ActiveRecord::TestCase
|
||||||
def test_destroy_all
|
def test_destroy_all
|
||||||
davids = Author.where(name: "David")
|
davids = Author.where(name: "David")
|
||||||
|
|
||||||
|
assert_difference("Author.count", -1) do
|
||||||
|
davids.destroy_all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_destroy_all_no_longer_returns_a_collection
|
||||||
|
davids = Author.where(name: "David")
|
||||||
|
|
||||||
|
assert_nil davids.destroy_all
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_destroy_all_deprecated_returns_collection
|
||||||
|
ActiveRecord::Base.destroy_all_in_batches = false
|
||||||
|
davids = Author.where(name: "David")
|
||||||
|
|
||||||
# Force load
|
# Force load
|
||||||
assert_equal [authors(:david)], davids.to_a
|
assert_equal [authors(:david)], davids.to_a
|
||||||
assert_predicate davids, :loaded?
|
assert_predicate davids, :loaded?
|
||||||
|
|
||||||
|
assert_deprecated do
|
||||||
assert_difference("Author.count", -1) do
|
assert_difference("Author.count", -1) do
|
||||||
destroyed = davids.destroy_all
|
destroyed = davids.destroy_all
|
||||||
assert_equal [authors(:david)], destroyed
|
assert_equal [authors(:david)], destroyed
|
||||||
assert_predicate destroyed.first, :frozen?
|
assert_predicate destroyed.first, :frozen?
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
assert_equal [], davids.to_a
|
assert_equal [], davids.to_a
|
||||||
assert_predicate davids, :loaded?
|
assert_predicate davids, :loaded?
|
||||||
|
ensure
|
||||||
|
ActiveRecord::Base.destroy_all_in_batches = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_delete_all
|
def test_delete_all
|
||||||
|
|
|
@ -1846,8 +1846,22 @@ class RelationTest < ActiveRecord::TestCase
|
||||||
|
|
||||||
assert_difference("Post.count", -3) { david.posts.destroy_by(body: "hello") }
|
assert_difference("Post.count", -3) { david.posts.destroy_by(body: "hello") }
|
||||||
|
|
||||||
destroyed = Author.destroy_by(id: david.id)
|
assert_difference("Author.count", -1) do
|
||||||
assert_equal [david], destroyed
|
Author.destroy_by(id: david.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_destroy_all_deprecated_return_value
|
||||||
|
ActiveRecord::Base.destroy_all_in_batches = false
|
||||||
|
david = authors(:david)
|
||||||
|
|
||||||
|
assert_deprecated do
|
||||||
|
assert_difference("Author.count", -1) do
|
||||||
|
assert_equal [david], Author.where(id: david.id).destroy_all
|
||||||
|
end
|
||||||
|
end
|
||||||
|
ensure
|
||||||
|
ActiveRecord::Base.destroy_all_in_batches = true
|
||||||
end
|
end
|
||||||
|
|
||||||
test "find_by with hash conditions returns the first matching record" do
|
test "find_by with hash conditions returns the first matching record" do
|
||||||
|
|
|
@ -511,6 +511,11 @@ The schema dumper adds two additional configuration options:
|
||||||
`fk_rails_` are not exported to the database schema dump.
|
`fk_rails_` are not exported to the database schema dump.
|
||||||
Defaults to `/^fk_rails_[0-9a-f]{10}$/`.
|
Defaults to `/^fk_rails_[0-9a-f]{10}$/`.
|
||||||
|
|
||||||
|
* `config.active_record.destroy_all_in_batches` ensures
|
||||||
|
ActiveRecord::Relation#destroy_all to perform the record's deletion in batches.
|
||||||
|
ActiveRecord::Relation#destroy_all won't longer return the collection of the deleted
|
||||||
|
records after enabling this option.
|
||||||
|
|
||||||
### Configuring Action Controller
|
### Configuring Action Controller
|
||||||
|
|
||||||
`config.action_controller` includes a number of configuration settings:
|
`config.action_controller` includes a number of configuration settings:
|
||||||
|
|
|
@ -43,3 +43,6 @@
|
||||||
# of the video).
|
# of the video).
|
||||||
# Rails.application.config.active_storage.video_preview_arguments =
|
# Rails.application.config.active_storage.video_preview_arguments =
|
||||||
# "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
|
# "-vf 'select=eq(n\\,0)+eq(key\\,1)+gt(scene\\,0.015),loop=loop=-1:size=2,trim=start_frame=1' -frames:v 1 -f image2"
|
||||||
|
#
|
||||||
|
# Enforce destroy in batches when calling ActiveRecord::Relation#destroy_all
|
||||||
|
Rails.application.config.active_record.destroy_all_in_batches = true
|
||||||
|
|
Loading…
Reference in a new issue