1
0
Fork 0
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:
Rafael França 2020-09-25 14:50:51 -04:00 committed by GitHub
commit 397bfb0e83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 635 additions and 11 deletions

View file

@ -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|

View file

@ -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*

View file

@ -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

View file

@ -76,6 +76,7 @@ module ActiveRecord
autoload :Translation
autoload :Validations
autoload :SecureToken
autoload :DestroyAssociationAsyncJob
eager_autoload do
autoload :ConnectionAdapters

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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]

View file

@ -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

View 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

View file

@ -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

View 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

View file

@ -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

View file

@ -205,5 +205,3 @@ module InTimeZone
ActiveRecord::Base.time_zone_aware_attributes = old_tz
end
end
require_relative "../../../tools/test_common"

View 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

View 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

View 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

View 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

View 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

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class DlKeyedHasMany < ActiveRecord::Base
self.primary_key = "many_key"
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class DlKeyedHasManyThrough < ActiveRecord::Base
self.primary_key = :through_key
end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class DlKeyedHasOne < ActiveRecord::Base
self.primary_key = "has_one_key"
end

View 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

View 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

View file

@ -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

View file

@ -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`.

View file

@ -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