mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Merge pull request #40157 from adrianna-chang-shopify/dependent-destroy-async
Offer dependent: :destroy_async for associations
This commit is contained in:
commit
397bfb0e83
35 changed files with 635 additions and 11 deletions
|
@ -34,6 +34,10 @@ module ActiveJob
|
|||
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
||||
include ActiveJob::TestHelper
|
||||
end
|
||||
|
||||
ActiveSupport.on_load(:active_record) do
|
||||
self.destroy_association_async_job = ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
initializer "active_job.set_reloader_hook" do |app|
|
||||
|
|
|
@ -1,3 +1,15 @@
|
|||
* Allow associations supporting the `dependent:` key to take `dependent: :destroy_async`.
|
||||
|
||||
```ruby
|
||||
class Account < ActiveRecord::Base
|
||||
belongs_to :supplier, dependent: :destroy_async
|
||||
end
|
||||
```
|
||||
|
||||
`:destroy_async` will enqueue a job to destroy associated records in the background.
|
||||
|
||||
*DHH*, *George Claghorn*, *Cory Gwin*, *Rafael Mendonça França*, *Adrianna Chang*
|
||||
|
||||
* Add `SKIP_TEST_DATABASE` environment variable to disable modifying the test database when `rails db:create` and `rails db:drop` are called.
|
||||
|
||||
*Jason Schweier*
|
||||
|
|
|
@ -61,6 +61,19 @@ end
|
|||
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
|
||||
}
|
||||
|
||||
namespace :integration do
|
||||
# Active Job Integration Tests
|
||||
namespace :active_job do
|
||||
Rake::TestTask.new(adapter => "#{adapter}:env") do |t|
|
||||
t.libs << "test"
|
||||
t.test_files = FileList["test/activejob/*_test.rb"]
|
||||
t.warning = true
|
||||
t.verbose = true
|
||||
t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
namespace :isolated do
|
||||
task adapter => "#{adapter}:env" do
|
||||
adapter_short = adapter[/^[a-z0-9]+/]
|
||||
|
@ -162,7 +175,7 @@ end
|
|||
end
|
||||
|
||||
# Make sure the adapter test evaluates the env setting task
|
||||
task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"]
|
||||
task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}", "test:integration:active_job:#{adapter}"]
|
||||
task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
|
||||
end
|
||||
|
||||
|
|
|
@ -76,6 +76,7 @@ module ActiveRecord
|
|||
autoload :Translation
|
||||
autoload :Validations
|
||||
autoload :SecureToken
|
||||
autoload :DestroyAssociationAsyncJob
|
||||
|
||||
eager_autoload do
|
||||
autoload :ConnectionAdapters
|
||||
|
|
|
@ -1371,6 +1371,7 @@ module ActiveRecord
|
|||
#
|
||||
# * <tt>nil</tt> do nothing (default).
|
||||
# * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
|
||||
# * <tt>:destroy_async</tt> destroys all the associated objects in a background job.
|
||||
# * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
|
||||
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Polymorphic type will also be nullified
|
||||
# on polymorphic associations. Callbacks are not executed.
|
||||
|
@ -1437,6 +1438,9 @@ module ActiveRecord
|
|||
# association objects.
|
||||
# [:strict_loading]
|
||||
# Enforces strict loading every time the associated record is loaded through this association.
|
||||
# [:ensuring_owner_was]
|
||||
# Specifies an instance method to be called on the owner. The method must return true in order for the
|
||||
# associated records to be deleted in a background job.
|
||||
#
|
||||
# Option examples:
|
||||
# has_many :comments, -> { order("posted_on") }
|
||||
|
@ -1519,6 +1523,7 @@ module ActiveRecord
|
|||
#
|
||||
# * <tt>nil</tt> do nothing (default).
|
||||
# * <tt>:destroy</tt> causes the associated object to also be destroyed
|
||||
# * <tt>:destroy_async</tt> causes all the associated object to be destroyed in a background job.
|
||||
# * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
|
||||
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Polymorphic type column is also nullified
|
||||
# on polymorphic associations. Callbacks are not executed.
|
||||
|
@ -1579,6 +1584,9 @@ module ActiveRecord
|
|||
# +:inverse_of+ to avoid an extra query during validation.
|
||||
# [:strict_loading]
|
||||
# Enforces strict loading every time the associated record is loaded through this association.
|
||||
# [:ensuring_owner_was]
|
||||
# Specifies an instance method to be called on the owner. The method must return true in order for the
|
||||
# associated records to be deleted in a background job.
|
||||
#
|
||||
# Option examples:
|
||||
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
|
||||
|
@ -1673,7 +1681,8 @@ module ActiveRecord
|
|||
# By default this is +id+.
|
||||
# [:dependent]
|
||||
# If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
||||
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method.
|
||||
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. If set to
|
||||
# <tt>:destroy_async</tt>, the associated object is scheduled to be destroyed in a background job.
|
||||
# This option should not be specified when #belongs_to is used in conjunction with
|
||||
# a #has_many relationship on another class because of the potential to leave
|
||||
# orphaned records behind.
|
||||
|
@ -1727,6 +1736,9 @@ module ActiveRecord
|
|||
# be initialized with a particular record before validation.
|
||||
# [:strict_loading]
|
||||
# Enforces strict loading every time the associated record is loaded through this association.
|
||||
# [:ensuring_owner_was]
|
||||
# Specifies an instance method to be called on the owner. The method must return true in order for the
|
||||
# associated records to be deleted in a background job.
|
||||
#
|
||||
# Option examples:
|
||||
# belongs_to :firm, foreign_key: "client_of"
|
||||
|
|
|
@ -321,6 +321,10 @@ module ActiveRecord
|
|||
klass.scope_attributes? ||
|
||||
reflection.source_reflection.active_record.default_scopes.any?
|
||||
end
|
||||
|
||||
def enqueue_destroy_association(options)
|
||||
owner.class.destroy_association_async_job&.perform_later(**options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,18 @@ module ActiveRecord
|
|||
when :destroy
|
||||
target.destroy
|
||||
raise ActiveRecord::Rollback unless target.destroyed?
|
||||
when :destroy_async
|
||||
id = owner.send(reflection.foreign_key.to_sym)
|
||||
primary_key_column = reflection.active_record_primary_key.to_sym
|
||||
|
||||
enqueue_destroy_association(
|
||||
owner_model_name: owner.class.to_s,
|
||||
owner_id: owner.id,
|
||||
association_class: reflection.klass.to_s,
|
||||
association_ids: [id],
|
||||
association_primary_key_column: primary_key_column,
|
||||
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
|
||||
)
|
||||
else
|
||||
target.send(options[:dependent])
|
||||
end
|
||||
|
|
|
@ -74,7 +74,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
|
||||
def self.define_callbacks(model, reflection)
|
||||
if dependent = reflection.options[:dependent]
|
||||
check_dependent_options(dependent)
|
||||
check_dependent_options(dependent, model)
|
||||
add_destroy_callbacks(model, reflection)
|
||||
end
|
||||
|
||||
|
@ -120,7 +120,11 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def self.check_dependent_options(dependent)
|
||||
def self.check_dependent_options(dependent, model)
|
||||
if dependent == :destroy_async && !model.destroy_association_async_job
|
||||
err_message = "ActiveJob is required to use destroy_async on associations"
|
||||
raise ActiveRecord::ActiveJobRequiredError, err_message
|
||||
end
|
||||
unless valid_dependent_options.include? dependent
|
||||
raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
|
||||
end
|
||||
|
|
|
@ -9,11 +9,12 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
def self.valid_options(options)
|
||||
valid = super + [:counter_cache, :optional, :default]
|
||||
valid += [:polymorphic, :foreign_type] if options[:polymorphic]
|
||||
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
|
||||
valid
|
||||
end
|
||||
|
||||
def self.valid_dependent_options
|
||||
[:destroy, :delete]
|
||||
[:destroy, :delete, :destroy_async]
|
||||
end
|
||||
|
||||
def self.define_callbacks(model, reflection)
|
||||
|
|
|
@ -7,14 +7,15 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
end
|
||||
|
||||
def self.valid_options(options)
|
||||
valid = super + [:counter_cache, :join_table, :index_errors]
|
||||
valid = super + [:counter_cache, :join_table, :index_errors, :ensuring_owner_was]
|
||||
valid += [:as, :foreign_type] if options[:as]
|
||||
valid += [:through, :source, :source_type] if options[:through]
|
||||
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
|
||||
valid
|
||||
end
|
||||
|
||||
def self.valid_dependent_options
|
||||
[:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
|
||||
[:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception, :destroy_async]
|
||||
end
|
||||
|
||||
private_class_method :macro, :valid_options, :valid_dependent_options
|
||||
|
|
|
@ -9,12 +9,13 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
def self.valid_options(options)
|
||||
valid = super
|
||||
valid += [:as, :foreign_type] if options[:as]
|
||||
valid += [:ensuring_owner_was] if options[:dependent] == :destroy_async
|
||||
valid += [:through, :source, :source_type] if options[:through]
|
||||
valid
|
||||
end
|
||||
|
||||
def self.valid_dependent_options
|
||||
[:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
|
||||
[:destroy, :destroy_async, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
|
||||
end
|
||||
|
||||
def self.define_callbacks(model, reflection)
|
||||
|
|
|
@ -26,6 +26,28 @@ module ActiveRecord
|
|||
# No point in executing the counter update since we're going to destroy the parent anyway
|
||||
load_target.each { |t| t.destroyed_by_association = reflection }
|
||||
destroy_all
|
||||
when :destroy_async
|
||||
load_target.each do |t|
|
||||
t.destroyed_by_association = reflection
|
||||
end
|
||||
|
||||
unless target.empty?
|
||||
association_class = target.first.class
|
||||
primary_key_column = association_class.primary_key.to_sym
|
||||
|
||||
ids = target.collect do |assoc|
|
||||
assoc.send(primary_key_column)
|
||||
end
|
||||
|
||||
enqueue_destroy_association(
|
||||
owner_model_name: owner.class.to_s,
|
||||
owner_id: owner.id,
|
||||
association_class: association_class.to_s,
|
||||
association_ids: ids,
|
||||
association_primary_key_column: primary_key_column,
|
||||
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
|
||||
)
|
||||
end
|
||||
else
|
||||
delete_all
|
||||
end
|
||||
|
|
|
@ -32,6 +32,18 @@ module ActiveRecord
|
|||
target.destroyed_by_association = reflection
|
||||
target.destroy
|
||||
throw(:abort) unless target.destroyed?
|
||||
when :destroy_async
|
||||
primary_key_column = target.class.primary_key.to_sym
|
||||
id = target.send(primary_key_column)
|
||||
|
||||
enqueue_destroy_association(
|
||||
owner_model_name: owner.class.to_s,
|
||||
owner_id: owner.id,
|
||||
association_class: reflection.klass.to_s,
|
||||
association_ids: [id],
|
||||
association_primary_key_column: primary_key_column,
|
||||
ensuring_owner_was_method: options.fetch(:ensuring_owner_was, nil)
|
||||
)
|
||||
when :nullify
|
||||
target.update_columns(nullified_owner_attributes) if target.persisted?
|
||||
end
|
||||
|
|
|
@ -26,6 +26,18 @@ module ActiveRecord
|
|||
# their relevant queries. Defaults to false.
|
||||
mattr_accessor :verbose_query_logs, instance_writer: false, default: false
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
#
|
||||
# Specifies the names of the queues used by background jobs.
|
||||
mattr_accessor :queues, instance_accessor: false, default: {}
|
||||
|
||||
##
|
||||
# :singleton-method:
|
||||
#
|
||||
# Specifies the job used to destroy associations in the background
|
||||
class_attribute :destroy_association_async_job, instance_writer: false, instance_predicate: false, default: false
|
||||
|
||||
##
|
||||
# Contains the database configuration - as is typically stored in config/database.yml -
|
||||
# as an ActiveRecord::DatabaseConfigurations object.
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveRecord
|
||||
class DestroyAssociationAsyncError < StandardError
|
||||
end
|
||||
|
||||
# Job to destroy the records associated with a destroyed record in background.
|
||||
class DestroyAssociationAsyncJob < ActiveJob::Base
|
||||
queue_as { ActiveRecord::Base.queues[:destroy] }
|
||||
|
||||
discard_on ActiveJob::DeserializationError
|
||||
|
||||
def perform(
|
||||
owner_model_name: nil, owner_id: nil,
|
||||
association_class: nil, association_ids: nil, association_primary_key_column: nil,
|
||||
ensuring_owner_was_method: nil
|
||||
)
|
||||
association_model = association_class.constantize
|
||||
owner_class = owner_model_name.constantize
|
||||
owner = owner_class.find_by(owner_class.primary_key.to_sym => owner_id)
|
||||
|
||||
if !owner_destroyed?(owner, ensuring_owner_was_method)
|
||||
raise DestroyAssociationAsyncError, "owner record not destroyed"
|
||||
end
|
||||
|
||||
association_model.where(association_primary_key_column => association_ids).find_each do |r|
|
||||
r.destroy
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def owner_destroyed?(owner, ensuring_owner_was_method)
|
||||
!owner || (ensuring_owner_was_method && owner.public_send(ensuring_owner_was_method))
|
||||
end
|
||||
end
|
||||
end
|
|
@ -7,6 +7,10 @@ module ActiveRecord
|
|||
class ActiveRecordError < StandardError
|
||||
end
|
||||
|
||||
# Raised when trying to use a feature in Active Record which requires Active Job but the gem is not present.
|
||||
class ActiveJobRequiredError < ActiveRecordError
|
||||
end
|
||||
|
||||
# Raised when the single-table inheritance mechanism fails to locate the subclass
|
||||
# (for example due to improper usage of column that
|
||||
# {ActiveRecord::Base.inheritance_column}[rdoc-ref:ModelSchema::ClassMethods#inheritance_column]
|
||||
|
|
|
@ -34,6 +34,8 @@ module ActiveRecord
|
|||
config.active_record.sqlite3 = ActiveSupport::OrderedOptions.new
|
||||
config.active_record.sqlite3.represent_boolean_as_integer = nil
|
||||
|
||||
config.active_record.queues = ActiveSupport::InheritableOptions.new(destroy: :active_record_destroy)
|
||||
|
||||
config.eager_load_namespaces << ActiveRecord
|
||||
|
||||
rake_tasks do
|
||||
|
|
234
activerecord/test/activejob/destroy_association_async_test.rb
Normal file
234
activerecord/test/activejob/destroy_association_async_test.rb
Normal file
|
@ -0,0 +1,234 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "activejob/helper"
|
||||
|
||||
require "models/book_destroy_async"
|
||||
require "models/essay_destroy_async"
|
||||
require "models/tag"
|
||||
require "models/tagging"
|
||||
require "models/essay"
|
||||
require "models/category"
|
||||
require "models/post"
|
||||
require "models/content"
|
||||
require "models/destroy_async_parent"
|
||||
require "models/destroy_async_parent_soft_delete"
|
||||
require "models/dl_keyed_belongs_to"
|
||||
require "models/dl_keyed_belongs_to_soft_delete"
|
||||
require "models/dl_keyed_has_one"
|
||||
require "models/dl_keyed_join"
|
||||
require "models/dl_keyed_has_many"
|
||||
require "models/dl_keyed_has_many_through"
|
||||
|
||||
class DestroyAssociationAsyncTest < ActiveRecord::TestCase
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
test "destroying a record destroys the has_many :through records using a job" do
|
||||
tag = Tag.create!(name: "Der be treasure")
|
||||
tag2 = Tag.create!(name: "Der be rum")
|
||||
book = BookDestroyAsync.create!
|
||||
book.tags << [tag, tag2]
|
||||
book.save!
|
||||
|
||||
book.destroy
|
||||
|
||||
assert_difference -> { Tag.count }, -2 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
test "destroying a scoped has_many through only deletes within the scope deleted" do
|
||||
tag = Tag.create!(name: "Der be treasure")
|
||||
tag2 = Tag.create!(name: "Der be rum")
|
||||
parent = BookDestroyAsyncWithScopedTags.create!
|
||||
parent.tags << [tag, tag2]
|
||||
parent.save!
|
||||
|
||||
parent.reload # force the association to be reloaded
|
||||
|
||||
parent.destroy
|
||||
|
||||
assert_difference -> { Tag.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
assert_raises ActiveRecord::RecordNotFound do
|
||||
tag2.reload
|
||||
end
|
||||
assert tag.reload
|
||||
end
|
||||
|
||||
test "enqueues the has_many through to be deleted with custom primary key" do
|
||||
dl_keyed_has_many = DlKeyedHasManyThrough.create!
|
||||
dl_keyed_has_many2 = DlKeyedHasManyThrough.create!
|
||||
parent = DestroyAsyncParent.create!
|
||||
parent.dl_keyed_has_many_through << [dl_keyed_has_many2, dl_keyed_has_many]
|
||||
parent.save!
|
||||
parent.destroy
|
||||
|
||||
assert_difference -> { DlKeyedJoin.count }, -2 do
|
||||
assert_difference -> { DlKeyedHasManyThrough.count }, -2 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "belongs to" do
|
||||
essay = EssayDestroyAsync.create!(name: "Der be treasure")
|
||||
book = BookDestroyAsync.create!(name: "Arr, matey!")
|
||||
essay.book = book
|
||||
essay.save!
|
||||
essay.destroy
|
||||
|
||||
assert_difference -> { BookDestroyAsync.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
test "enqueues belongs_to to be deleted with custom primary key" do
|
||||
belongs = DlKeyedBelongsTo.create!
|
||||
parent = DestroyAsyncParent.create!
|
||||
belongs.destroy_async_parent = parent
|
||||
belongs.save!
|
||||
belongs.destroy
|
||||
|
||||
assert_difference -> { DestroyAsyncParent.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
test "has_one" do
|
||||
content = Content.create(title: "hello")
|
||||
book = BookDestroyAsync.create!(name: "Arr, matey!")
|
||||
book.content = content
|
||||
book.save!
|
||||
book.destroy
|
||||
|
||||
assert_difference -> { Content.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
test "enqueues has_one to be deleted with custom primary key" do
|
||||
child = DlKeyedHasOne.create!
|
||||
parent = DestroyAsyncParent.create!
|
||||
parent.dl_keyed_has_one = child
|
||||
parent.save!
|
||||
parent.destroy
|
||||
|
||||
assert_difference -> { DlKeyedHasOne.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
test "has_many" do
|
||||
essay = EssayDestroyAsync.create!(name: "Der be treasure")
|
||||
essay2 = EssayDestroyAsync.create!(name: "Der be rum")
|
||||
book = BookDestroyAsync.create!(name: "Arr, matey!")
|
||||
book.essays << [essay, essay2]
|
||||
book.save!
|
||||
book.destroy
|
||||
|
||||
assert_difference -> { EssayDestroyAsync.count }, -2 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
test "enqueues the has_many to be deleted with custom primary key" do
|
||||
dl_keyed_has_many = DlKeyedHasMany.new
|
||||
parent = DestroyAsyncParent.create!
|
||||
parent.dl_keyed_has_many << [dl_keyed_has_many]
|
||||
|
||||
parent.save!
|
||||
parent.destroy
|
||||
|
||||
assert_difference -> { DlKeyedHasMany.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
test "throw an error if the record is not actually deleted" do
|
||||
dl_keyed_has_many = DlKeyedHasMany.new
|
||||
parent = DestroyAsyncParent.create!
|
||||
parent.dl_keyed_has_many << [dl_keyed_has_many]
|
||||
|
||||
parent.save!
|
||||
DestroyAsyncParent.transaction do
|
||||
parent.destroy
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
assert_difference -> { DlKeyedHasMany.count }, 0 do
|
||||
assert_raises ActiveRecord::DestroyAssociationAsyncError do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "has many ensures function for parent" do
|
||||
tag = Tag.create!(name: "Der be treasure")
|
||||
tag2 = Tag.create!(name: "Der be rum")
|
||||
parent = DestroyAsyncParentSoftDelete.create!
|
||||
parent.tags << [tag, tag2]
|
||||
parent.save!
|
||||
|
||||
parent.run_callbacks(:destroy)
|
||||
|
||||
assert_difference -> { Tag.count }, -0 do
|
||||
assert_raises ActiveRecord::DestroyAssociationAsyncError do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
parent.destroy
|
||||
assert_difference -> { Tag.count }, -2 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
test "has one ensures function for parent" do
|
||||
child = DlKeyedHasOne.create!
|
||||
parent = DestroyAsyncParentSoftDelete.create!
|
||||
parent.dl_keyed_has_one = child
|
||||
parent.save!
|
||||
|
||||
parent.run_callbacks(:destroy)
|
||||
|
||||
assert_difference -> { DlKeyedHasOne.count }, -0 do
|
||||
assert_raises ActiveRecord::DestroyAssociationAsyncError do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
parent.destroy
|
||||
assert_difference -> { DlKeyedHasOne.count }, -1 do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
||||
|
||||
test "enqueues belongs_to to be deleted with ensuring function" do
|
||||
belongs = DlKeyedBelongsToSoftDelete.create!
|
||||
parent = DestroyAsyncParentSoftDelete.create!
|
||||
belongs.destroy_async_parent_soft_delete = parent
|
||||
belongs.save!
|
||||
belongs.run_callbacks(:destroy)
|
||||
|
||||
assert_raises ActiveRecord::DestroyAssociationAsyncError do
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
|
||||
assert_not parent.reload.deleted?
|
||||
|
||||
belongs.destroy
|
||||
perform_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
assert parent.reload.deleted?
|
||||
end
|
||||
|
||||
test "Don't enqueue with no relations" do
|
||||
parent = DestroyAsyncParent.create!
|
||||
parent.destroy
|
||||
|
||||
assert_no_enqueued_jobs only: ActiveRecord::DestroyAssociationAsyncJob
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cases/helper"
|
||||
|
||||
class UnusedDestroyAsync < ActiveRecord::Base
|
||||
self.destroy_association_async_job = nil
|
||||
end
|
||||
|
||||
class UnusedBelongsTo < ActiveRecord::Base
|
||||
self.destroy_association_async_job = nil
|
||||
end
|
||||
|
||||
class ActiveJobNotPresentTest < ActiveRecord::TestCase
|
||||
test "has_one dependent destroy_async requires activejob" do
|
||||
assert_raises ActiveRecord::ActiveJobRequiredError do
|
||||
UnusedDestroyAsync.has_one :unused_belongs_to, dependent: :destroy_async
|
||||
end
|
||||
end
|
||||
|
||||
test "has_many dependent destroy_async requires activejob" do
|
||||
assert_raises ActiveRecord::ActiveJobRequiredError do
|
||||
UnusedDestroyAsync.has_many :essay_destroy_asyncs, dependent: :destroy_async
|
||||
end
|
||||
end
|
||||
|
||||
test "belong_to dependent destroy_async requires activejob" do
|
||||
assert_raises ActiveRecord::ActiveJobRequiredError do
|
||||
UnusedBelongsTo.belongs_to :unused_destroy_asyncs, dependent: :destroy_async
|
||||
end
|
||||
end
|
||||
end
|
15
activerecord/test/activejob/helper.rb
Normal file
15
activerecord/test/activejob/helper.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "cases/helper"
|
||||
|
||||
require "global_id"
|
||||
GlobalID.app = "ActiveRecordExampleApp"
|
||||
ActiveRecord::Base.include GlobalID::Identification
|
||||
|
||||
require "active_job"
|
||||
ActiveJob::Base.queue_adapter = :test
|
||||
ActiveJob::Base.logger = ActiveSupport::Logger.new(nil)
|
||||
|
||||
require_relative "../../../tools/test_common"
|
||||
|
||||
ActiveRecord::Base.destroy_association_async_job = ActiveRecord::DestroyAssociationAsyncJob
|
|
@ -1110,7 +1110,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
error = assert_raise ArgumentError do
|
||||
Class.new(Author).belongs_to :special_author_address, dependent: :nullify
|
||||
end
|
||||
assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify"
|
||||
assert_equal error.message, "The :dependent option must be one of [:destroy, :delete, :destroy_async], but is :nullify"
|
||||
end
|
||||
|
||||
class DestroyableBook < ActiveRecord::Base
|
||||
|
|
|
@ -205,5 +205,3 @@ module InTimeZone
|
|||
ActiveRecord::Base.time_zone_aware_attributes = old_tz
|
||||
end
|
||||
end
|
||||
|
||||
require_relative "../../../tools/test_common"
|
||||
|
|
24
activerecord/test/models/book_destroy_async.rb
Normal file
24
activerecord/test/models/book_destroy_async.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BookDestroyAsync < ActiveRecord::Base
|
||||
self.table_name = "books"
|
||||
|
||||
has_many :taggings, as: :taggable, class_name: "Tagging"
|
||||
has_many :tags, through: :taggings, dependent: :destroy_async
|
||||
has_many :essays, dependent: :destroy_async, class_name: "EssayDestroyAsync", foreign_key: "book_id"
|
||||
has_one :content, dependent: :destroy_async
|
||||
|
||||
enum status: [:proposed, :written, :published]
|
||||
|
||||
def published!
|
||||
super
|
||||
"do publish work..."
|
||||
end
|
||||
end
|
||||
|
||||
class BookDestroyAsyncWithScopedTags < ActiveRecord::Base
|
||||
self.table_name = "books"
|
||||
|
||||
has_many :taggings, as: :taggable, class_name: "Tagging"
|
||||
has_many :tags, -> { where name: "Der be rum" }, through: :taggings, dependent: :destroy_async
|
||||
end
|
15
activerecord/test/models/destroy_async_parent.rb
Normal file
15
activerecord/test/models/destroy_async_parent.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DestroyAsyncParent < ActiveRecord::Base
|
||||
self.primary_key = "parent_id"
|
||||
|
||||
has_one :dl_keyed_has_one, dependent: :destroy_async,
|
||||
foreign_key: :destroy_async_parent_id, primary_key: :parent_id
|
||||
has_many :dl_keyed_has_many, dependent: :destroy_async,
|
||||
foreign_key: :many_key, primary_key: :parent_id
|
||||
has_many :dl_keyed_join, dependent: :destroy_async,
|
||||
foreign_key: :destroy_async_parent_id, primary_key: :joins_key
|
||||
has_many :dl_keyed_has_many_through,
|
||||
through: :dl_keyed_join, dependent: :destroy_async,
|
||||
foreign_key: :dl_has_many_through_key_id, primary_key: :through_key
|
||||
end
|
20
activerecord/test/models/destroy_async_parent_soft_delete.rb
Normal file
20
activerecord/test/models/destroy_async_parent_soft_delete.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DestroyAsyncParentSoftDelete < ActiveRecord::Base
|
||||
has_many :taggings, as: :taggable, class_name: "Tagging"
|
||||
has_many :tags, through: :taggings,
|
||||
dependent: :destroy_async,
|
||||
ensuring_owner_was: :deleted?
|
||||
|
||||
has_one :dl_keyed_has_one, dependent: :destroy_async,
|
||||
ensuring_owner_was: :deleted?
|
||||
|
||||
def deleted?
|
||||
deleted
|
||||
end
|
||||
|
||||
def destroy
|
||||
update(deleted: true)
|
||||
run_callbacks(:destroy)
|
||||
end
|
||||
end
|
13
activerecord/test/models/dl_keyed_belongs_to.rb
Normal file
13
activerecord/test/models/dl_keyed_belongs_to.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DlKeyedBelongsTo < ActiveRecord::Base
|
||||
self.primary_key = "belongs_key"
|
||||
belongs_to :destroy_async_parent,
|
||||
dependent: :destroy_async,
|
||||
foreign_key: :destroy_async_parent_id,
|
||||
primary_key: :parent_id,
|
||||
class_name: "DestroyAsyncParent"
|
||||
belongs_to :destroy_async_parent_soft_delete,
|
||||
dependent: :destroy_async,
|
||||
ensuring_owner_was: :deleted?, class_name: "DestroyAsyncParentSoftDelete"
|
||||
end
|
19
activerecord/test/models/dl_keyed_belongs_to_soft_delete.rb
Normal file
19
activerecord/test/models/dl_keyed_belongs_to_soft_delete.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "models/destroy_async_parent_soft_delete"
|
||||
|
||||
class DlKeyedBelongsToSoftDelete < ActiveRecord::Base
|
||||
belongs_to :destroy_async_parent_soft_delete,
|
||||
dependent: :destroy_async,
|
||||
ensuring_owner_was: :deleted?,
|
||||
class_name: "DestroyAsyncParentSoftDelete"
|
||||
|
||||
def deleted?
|
||||
deleted
|
||||
end
|
||||
|
||||
def destroy
|
||||
update(deleted: true)
|
||||
run_callbacks(:destroy)
|
||||
end
|
||||
end
|
5
activerecord/test/models/dl_keyed_has_many.rb
Normal file
5
activerecord/test/models/dl_keyed_has_many.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DlKeyedHasMany < ActiveRecord::Base
|
||||
self.primary_key = "many_key"
|
||||
end
|
5
activerecord/test/models/dl_keyed_has_many_through.rb
Normal file
5
activerecord/test/models/dl_keyed_has_many_through.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DlKeyedHasManyThrough < ActiveRecord::Base
|
||||
self.primary_key = :through_key
|
||||
end
|
5
activerecord/test/models/dl_keyed_has_one.rb
Normal file
5
activerecord/test/models/dl_keyed_has_one.rb
Normal file
|
@ -0,0 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DlKeyedHasOne < ActiveRecord::Base
|
||||
self.primary_key = "has_one_key"
|
||||
end
|
10
activerecord/test/models/dl_keyed_join.rb
Normal file
10
activerecord/test/models/dl_keyed_join.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class DlKeyedJoin < ActiveRecord::Base
|
||||
self.primary_key = "joins_key"
|
||||
|
||||
belongs_to :destroy_async_parent,
|
||||
primary_key: :parent_id
|
||||
belongs_to :dl_keyed_has_many_through,
|
||||
primary_key: :through_key
|
||||
end
|
6
activerecord/test/models/essay_destroy_async.rb
Normal file
6
activerecord/test/models/essay_destroy_async.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class EssayDestroyAsync < ActiveRecord::Base
|
||||
self.table_name = "essays"
|
||||
belongs_to :book, dependent: :destroy_async, class_name: "BookDestroyAsync"
|
||||
end
|
|
@ -120,6 +120,7 @@ ActiveRecord::Schema.define do
|
|||
t.datetime :published_on
|
||||
t.boolean :boolean_status
|
||||
t.index [:author_id, :name], unique: true
|
||||
t.integer :tags_count, default: 0
|
||||
t.index :isbn, where: "published_on IS NOT NULL", unique: true
|
||||
t.index "(lower(external_id))", unique: true if supports_expression_index?
|
||||
|
||||
|
@ -242,6 +243,8 @@ ActiveRecord::Schema.define do
|
|||
|
||||
create_table :content, force: true do |t|
|
||||
t.string :title
|
||||
t.belongs_to :book
|
||||
t.belongs_to :book_destroy_async
|
||||
end
|
||||
|
||||
create_table :content_positions, force: true do |t|
|
||||
|
@ -291,6 +294,50 @@ ActiveRecord::Schema.define do
|
|||
t.string :name
|
||||
end
|
||||
|
||||
create_table :destroy_async_parents, force: true, id: false do |t|
|
||||
t.primary_key :parent_id
|
||||
t.string :name
|
||||
t.integer :tags_count, default: 0
|
||||
end
|
||||
|
||||
create_table :destroy_async_parent_soft_deletes, force: true do |t|
|
||||
t.integer :tags_count, default: 0
|
||||
t.boolean :deleted
|
||||
end
|
||||
|
||||
create_table :dl_keyed_belongs_tos, force: true, id: false do |t|
|
||||
t.primary_key :belongs_key
|
||||
t.references :destroy_async_parent
|
||||
end
|
||||
|
||||
create_table :dl_keyed_belongs_to_soft_deletes, force: true do |t|
|
||||
t.references :destroy_async_parent_soft_delete,
|
||||
index: { name: :soft_del_parent }
|
||||
t.boolean :deleted
|
||||
end
|
||||
|
||||
create_table :dl_keyed_has_ones, force: true, id: false do |t|
|
||||
t.primary_key :has_one_key
|
||||
|
||||
t.references :destroy_async_parent
|
||||
t.references :destroy_async_parent_soft_delete
|
||||
end
|
||||
|
||||
create_table :dl_keyed_has_manies, force: true, id: false do |t|
|
||||
t.primary_key :many_key
|
||||
t.references :destroy_async_parent
|
||||
end
|
||||
|
||||
create_table :dl_keyed_has_many_throughs, force: true, id: false do |t|
|
||||
t.primary_key :through_key
|
||||
end
|
||||
|
||||
create_table :dl_keyed_joins, force: true, id: false do |t|
|
||||
t.primary_key :joins_key
|
||||
t.references :destroy_async_parent
|
||||
t.references :dl_keyed_has_many_through
|
||||
end
|
||||
|
||||
create_table :developers, force: true do |t|
|
||||
t.string :name
|
||||
t.string :first_name
|
||||
|
@ -361,6 +408,7 @@ ActiveRecord::Schema.define do
|
|||
t.string :writer_type
|
||||
t.string :category_id
|
||||
t.string :author_id
|
||||
t.references :book
|
||||
end
|
||||
|
||||
create_table :events, force: true do |t|
|
||||
|
@ -993,6 +1041,13 @@ ActiveRecord::Schema.define do
|
|||
t.integer :car_id
|
||||
end
|
||||
|
||||
create_table :unused_destroy_asyncs, force: true do |t|
|
||||
end
|
||||
|
||||
create_table :unused_belongs_to, force: true do |t|
|
||||
t.belongs_to :unused_destroy_async
|
||||
end
|
||||
|
||||
create_table :variants, force: true do |t|
|
||||
t.references :product
|
||||
t.string :name
|
||||
|
|
|
@ -464,6 +464,10 @@ in controllers and views. This defaults to `false`.
|
|||
* `config.active_record.has_many_inversing` enables setting the inverse record
|
||||
when traversing `belongs_to` to `has_many` associations.
|
||||
|
||||
* `config.active_record.destroy_association_async_job` allows specifying the job that will be used to destroy the associated records in background. It defaults to `ActiveRecord::DestroyAssociationAsyncJob`.
|
||||
|
||||
* `config.active_record.queues.destroy` allows specifying the Active Job queue to use for destroy jobs. It defaults to `:active_record_destroy`.
|
||||
|
||||
The MySQL adapter adds one additional configuration option:
|
||||
|
||||
* `ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans` controls whether Active Record will consider all `tinyint(1)` columns as booleans. Defaults to `true`.
|
||||
|
|
|
@ -2249,6 +2249,18 @@ module ApplicationTests
|
|||
assert_includes ActiveJob::Serializers.serializers, DummySerializer
|
||||
end
|
||||
|
||||
test "active record job queue is set" do
|
||||
app "development"
|
||||
|
||||
assert_equal ActiveSupport::InheritableOptions.new(destroy: :active_record_destroy), ActiveRecord::Base.queues
|
||||
end
|
||||
|
||||
test "destroy association async job should be loaded in configs" do
|
||||
app "development"
|
||||
|
||||
assert_equal ActiveRecord::DestroyAssociationAsyncJob, ActiveRecord::Base.destroy_association_async_job
|
||||
end
|
||||
|
||||
test "ActionView::Helpers::FormTagHelper.default_enforce_utf8 is false by default" do
|
||||
app "development"
|
||||
assert_equal false, ActionView::Helpers::FormTagHelper.default_enforce_utf8
|
||||
|
|
Loading…
Reference in a new issue