delete_recursively 1.1.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/delete_recursively/active_record_extensions.rb +57 -0
- data/lib/delete_recursively/associated_class_finder.rb +57 -0
- data/lib/delete_recursively/dependent_id_finder.rb +47 -0
- data/lib/delete_recursively/railtie.rb +5 -0
- data/lib/delete_recursively/version.rb +1 -1
- data/lib/delete_recursively.rb +44 -143
- metadata +9 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b030ca2cd0433b8f6edbe713ec5e75651cfd8b17b09ce8080c0fe9867b85799d
|
4
|
+
data.tar.gz: db4adfc88b256876c756cd7f216192b324c4c2a56892c57e00961d19c8121fea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 90ec72416540144c258e6234142d9877596e3e42d17489d539b80ddaa1fe7cf80d2a5adb19269e4e3759393c83527edd994468fb220611a9a3752da4d37018c1
|
7
|
+
data.tar.gz: f4f0e2432412bf80ca7275d5fe43e03317643e35d6d1bae4cf890057915c90dc57cb682066366c5fe9d51f2a1e2c086810cad9c3e65e1f55561727f4ccdaf802
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# override ::valid_dependent_options to make the new
|
2
|
+
# dependent option available to Association::Builder classes
|
3
|
+
module DeleteRecursively::OptionPermission
|
4
|
+
def valid_dependent_options
|
5
|
+
super + [DeleteRecursively::NEW_DEPENDENT_OPTION]
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
# override Association#handle_dependency to apply the new option if it is set
|
10
|
+
module DeleteRecursively::DependencyHandling
|
11
|
+
def handle_dependency
|
12
|
+
if DeleteRecursively.enabled_for?(self)
|
13
|
+
delete_dependencies_recursively
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete_dependencies_recursively(force: false)
|
20
|
+
if reflection.belongs_to?
|
21
|
+
# Special case. The owner is already destroyed at this point,
|
22
|
+
# so we cannot use the efficient ::dependent_ids lookup. Note that this
|
23
|
+
# only happens for a single entry-record on #destroy, though.
|
24
|
+
return unless target = load_target
|
25
|
+
|
26
|
+
DeleteRecursively.delete_records_recursively(target.class, target.id, force: force)
|
27
|
+
else
|
28
|
+
DeleteRecursively.delete_recursively(reflection, nil, owner.id, force: force)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
require 'active_record'
|
34
|
+
|
35
|
+
module ActiveRecord
|
36
|
+
module Associations
|
37
|
+
%w[BelongsTo HasMany HasOne].each do |assoc_name|
|
38
|
+
assoc_builder = Builder.const_get(assoc_name)
|
39
|
+
assoc_builder.singleton_class.prepend(DeleteRecursively::OptionPermission)
|
40
|
+
|
41
|
+
assoc_class = const_get("#{assoc_name}Association")
|
42
|
+
assoc_class.prepend(DeleteRecursively::DependencyHandling)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class Base
|
47
|
+
def delete_recursively(force: false)
|
48
|
+
DeleteRecursively.delete_records_recursively(self.class, id, force: force)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class Relation
|
53
|
+
def delete_all_recursively(force: false)
|
54
|
+
DeleteRecursively.delete_records_recursively(klass, ids, force: force)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module DeleteRecursively::AssociatedClassFinder
|
2
|
+
class << self
|
3
|
+
def call(reflection)
|
4
|
+
cache[reflection] ||= find_classes(reflection)
|
5
|
+
end
|
6
|
+
|
7
|
+
def clear_cache
|
8
|
+
cache.clear
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def cache
|
14
|
+
@cache ||= {}.tap(&:compare_by_identity)
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_classes(reflection)
|
18
|
+
result =
|
19
|
+
if reflection.polymorphic?
|
20
|
+
find_classes_for_polymorphic_reflection(reflection)
|
21
|
+
else
|
22
|
+
[reflection.klass]
|
23
|
+
end.compact
|
24
|
+
|
25
|
+
result.empty? && warn_empty_result(reflection)
|
26
|
+
|
27
|
+
result
|
28
|
+
end
|
29
|
+
|
30
|
+
# This ignores relatives where the inverse relation is not defined.
|
31
|
+
# The alternative to this approach would be to expensively select
|
32
|
+
# all distinct values from the *_type column:
|
33
|
+
# reflection.active_record.distinct.pluck(reflection.foreign_type)
|
34
|
+
def find_classes_for_polymorphic_reflection(reflection)
|
35
|
+
ActiveRecord::Base.descendants.select do |klass|
|
36
|
+
klass.reflect_on_all_associations.any? do |other_ref|
|
37
|
+
next other_ref.inverse_of == reflection unless other_ref.polymorphic?
|
38
|
+
|
39
|
+
# check if its a bi-directional polymorphic association
|
40
|
+
begin
|
41
|
+
other_ref.polymorphic_inverse_of(reflection.active_record)
|
42
|
+
rescue ActiveRecord::InverseOfAssociationNotFoundError
|
43
|
+
next
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def warn_empty_result(reflection)
|
50
|
+
warn "#{self} could not find associated class(es) for "\
|
51
|
+
"#{reflection.active_record}##{reflection.name}. "\
|
52
|
+
"You might need to define the inverse association(s) or, "\
|
53
|
+
"if they are already defined, add `as :#{reflection.name}` or "\
|
54
|
+
"`inverse_of :#{reflection.name}` to them."
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module DeleteRecursively::DependentIdFinder
|
2
|
+
class << self
|
3
|
+
def call(owner_ids, reflection, assoc_class)
|
4
|
+
owner_class = reflection.active_record
|
5
|
+
|
6
|
+
if reflection.belongs_to?
|
7
|
+
owners = owner_class.where(owner_class.primary_key => owner_ids)
|
8
|
+
if reflection.polymorphic?
|
9
|
+
owners = owners.where(reflection.foreign_type => assoc_class.to_s)
|
10
|
+
end
|
11
|
+
owners.pluck(reflection.foreign_key).compact
|
12
|
+
elsif reflection.through_reflection
|
13
|
+
dependent_ids_with_through_option(owner_ids, reflection)
|
14
|
+
else # plain `has_many` or `has_one`
|
15
|
+
owner_foreign_key = foreign_key(owner_class, reflection)
|
16
|
+
reflection.klass.where(owner_foreign_key => owner_ids).ids
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def dependent_ids_with_through_option(owner_ids, reflection)
|
23
|
+
through_reflection = reflection.through_reflection
|
24
|
+
owner_foreign_key = foreign_key(reflection.active_record, through_reflection)
|
25
|
+
|
26
|
+
dependent_class = reflection.klass
|
27
|
+
dependent_through_reflection = inverse_through_reflection(reflection)
|
28
|
+
dependent_foreign_key =
|
29
|
+
foreign_key(dependent_class, dependent_through_reflection)
|
30
|
+
|
31
|
+
through_reflection.klass
|
32
|
+
.where(owner_foreign_key => owner_ids)
|
33
|
+
.pluck(dependent_foreign_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
def foreign_key(owner_class, reflection)
|
37
|
+
reflection && reflection.foreign_key || owner_class.to_s.foreign_key
|
38
|
+
end
|
39
|
+
|
40
|
+
def inverse_through_reflection(reflection)
|
41
|
+
through_class = reflection.through_reflection.klass
|
42
|
+
reflection.klass.reflect_on_all_associations
|
43
|
+
.map(&:through_reflection)
|
44
|
+
.find { |thr_ref| thr_ref && thr_ref.klass == through_class }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/delete_recursively.rb
CHANGED
@@ -6,159 +6,59 @@
|
|
6
6
|
# Adds a new dependent: option to ActiveRecord associations.
|
7
7
|
#
|
8
8
|
module DeleteRecursively
|
9
|
-
require_relative File.join('delete_recursively', 'version')
|
10
|
-
|
11
9
|
NEW_DEPENDENT_OPTION = :delete_recursively
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
# override Association#handle_dependency to apply the new option if it is set
|
22
|
-
module DependencyHandling
|
23
|
-
def handle_dependency
|
24
|
-
if DeleteRecursively.enabled_for?(self)
|
25
|
-
delete_dependencies_recursively
|
26
|
-
else
|
27
|
-
super
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
def delete_dependencies_recursively(force: false)
|
32
|
-
if reflection.belongs_to?
|
33
|
-
# Special case. The owner is already destroyed at this point,
|
34
|
-
# so we cannot use the efficient ::dependent_ids lookup. Note that this
|
35
|
-
# only happens for a single entry-record on #destroy, though.
|
36
|
-
return unless target = load_target
|
37
|
-
|
38
|
-
DeleteRecursively.delete_records_recursively(target.class, target.id, force: force)
|
39
|
-
else
|
40
|
-
DeleteRecursively.delete_recursively(reflection, owner.class, owner.id, force: force)
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
11
|
+
require_relative File.join('delete_recursively', 'active_record_extensions')
|
12
|
+
require_relative File.join('delete_recursively', 'associated_class_finder')
|
13
|
+
require_relative File.join('delete_recursively', 'dependent_id_finder')
|
14
|
+
require_relative File.join('delete_recursively', 'railtie') if defined?(::Rails::Railtie)
|
15
|
+
require_relative File.join('delete_recursively', 'version')
|
44
16
|
|
45
17
|
class << self
|
46
|
-
def delete_recursively(reflection,
|
18
|
+
def delete_recursively(reflection, _legacy_arg, owner_ids, seen: [], force: false)
|
19
|
+
owner_ids = Array(owner_ids)
|
20
|
+
return if owner_ids.empty?
|
21
|
+
|
47
22
|
# Dependent deletion can be bi-directional, so we need to avoid a loop.
|
48
|
-
|
23
|
+
# Note, however, that an association could be reached multiple times, from
|
24
|
+
# different starting points within the association tree, and having
|
25
|
+
# different owner_ids. In this case, we do need to go through it again.
|
26
|
+
recursion_identifier = [reflection, owner_ids]
|
27
|
+
return if seen.include?(recursion_identifier)
|
49
28
|
|
50
|
-
seen <<
|
29
|
+
seen << recursion_identifier
|
51
30
|
|
52
|
-
|
31
|
+
AssociatedClassFinder.call(reflection).each do |assoc_class|
|
53
32
|
record_ids = nil # fetched only when needed for recursion, deletion, or both
|
54
33
|
|
55
34
|
if recurse_on?(reflection)
|
56
|
-
record_ids =
|
57
|
-
|
58
|
-
delete_recursively(subref,
|
35
|
+
record_ids = DependentIdFinder.call(owner_ids, reflection, assoc_class)
|
36
|
+
assoc_class.reflect_on_all_associations.each do |subref|
|
37
|
+
delete_recursively(subref, nil, record_ids, seen: seen, force: force)
|
59
38
|
end
|
60
39
|
end
|
61
40
|
|
62
|
-
if dest_method = destructive_method(reflection,
|
63
|
-
record_ids ||=
|
64
|
-
|
41
|
+
if dest_method = destructive_method(reflection, force: force)
|
42
|
+
record_ids ||= DependentIdFinder.call(owner_ids, reflection, assoc_class)
|
43
|
+
assoc_class.send(dest_method, record_ids)
|
65
44
|
end
|
66
45
|
end
|
67
46
|
end
|
68
47
|
|
69
|
-
def associated_classes(reflection)
|
70
|
-
if reflection.polymorphic?
|
71
|
-
# This ignores relatives where the inverse relation is not defined.
|
72
|
-
# The alternative to this approach would be to expensively select
|
73
|
-
# all distinct values from the *_type column:
|
74
|
-
# reflection.active_record.distinct.pluck(reflection.foreign_type)
|
75
|
-
ActiveRecord::Base.descendants.select do |klass|
|
76
|
-
klass.reflect_on_all_associations
|
77
|
-
.any? { |ref| ref.inverse_of == reflection }
|
78
|
-
end
|
79
|
-
else
|
80
|
-
[reflection.klass]
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
48
|
def delete_records_recursively(record_class, record_ids, force: false)
|
85
49
|
record_class.reflect_on_all_associations.each do |ref|
|
86
|
-
delete_recursively(ref,
|
50
|
+
delete_recursively(ref, nil, record_ids, force: force)
|
87
51
|
end
|
88
52
|
record_class.delete(record_ids)
|
89
53
|
end
|
90
54
|
|
91
|
-
def destructive_method(reflection, record_class, record_ids, force: false)
|
92
|
-
if deleting?(reflection) || force && destructive?(reflection)
|
93
|
-
:delete
|
94
|
-
elsif destructive?(reflection)
|
95
|
-
:destroy
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
def recurse_on?(reflection)
|
100
|
-
enabled_for?(reflection) || destructive?(reflection)
|
101
|
-
end
|
102
|
-
|
103
|
-
def enabled_for?(reflection)
|
104
|
-
reflection.options[:dependent] == NEW_DEPENDENT_OPTION
|
105
|
-
end
|
106
|
-
|
107
|
-
def destructive?(reflection)
|
108
|
-
%i[destroy destroy_all].include?(reflection.options[:dependent])
|
109
|
-
end
|
110
|
-
|
111
|
-
def deleting?(reflection)
|
112
|
-
[:delete, :delete_all, NEW_DEPENDENT_OPTION].include?(reflection.options[:dependent])
|
113
|
-
end
|
114
|
-
|
115
|
-
def dependent_ids(owner_class, owner_ids, reflection, assoc_class = nil)
|
116
|
-
if reflection.belongs_to?
|
117
|
-
owners = owner_class.where(owner_class.primary_key => owner_ids)
|
118
|
-
if reflection.polymorphic?
|
119
|
-
owners = owners.where(reflection.foreign_type => assoc_class.to_s)
|
120
|
-
end
|
121
|
-
owners.pluck(reflection.foreign_key).compact
|
122
|
-
elsif reflection.through_reflection
|
123
|
-
dependent_ids_with_through_option(owner_class, owner_ids, reflection)
|
124
|
-
else # plain `has_many` or `has_one`
|
125
|
-
owner_foreign_key = foreign_key(owner_class, reflection)
|
126
|
-
reflection.klass.where(owner_foreign_key => owner_ids).ids
|
127
|
-
end
|
128
|
-
end
|
129
|
-
|
130
|
-
def dependent_ids_with_through_option(owner_class, owner_ids, reflection)
|
131
|
-
through_reflection = reflection.through_reflection
|
132
|
-
owner_foreign_key = foreign_key(owner_class, through_reflection)
|
133
|
-
|
134
|
-
dependent_class = reflection.klass
|
135
|
-
dependent_through_reflection = inverse_through_reflection(reflection)
|
136
|
-
dependent_foreign_key =
|
137
|
-
foreign_key(dependent_class, dependent_through_reflection)
|
138
|
-
|
139
|
-
through_reflection.klass
|
140
|
-
.where(owner_foreign_key => owner_ids)
|
141
|
-
.pluck(dependent_foreign_key)
|
142
|
-
end
|
143
|
-
|
144
|
-
def inverse_through_reflection(reflection)
|
145
|
-
through_class = reflection.through_reflection.klass
|
146
|
-
reflection.klass.reflect_on_all_associations
|
147
|
-
.map(&:through_reflection)
|
148
|
-
.find { |thr_ref| thr_ref && thr_ref.klass == through_class }
|
149
|
-
end
|
150
|
-
|
151
|
-
def foreign_key(owner_class, reflection)
|
152
|
-
reflection && reflection.foreign_key || owner_class.to_s.foreign_key
|
153
|
-
end
|
154
|
-
|
155
55
|
def all(record_class, criteria = {}, seen = [])
|
156
56
|
return if seen.include?(record_class)
|
157
57
|
|
158
58
|
seen << record_class
|
159
59
|
|
160
60
|
record_class.reflect_on_all_associations.each do |reflection|
|
161
|
-
|
61
|
+
AssociatedClassFinder.call(reflection).each do |assoc_class|
|
162
62
|
if recurse_on?(reflection)
|
163
63
|
all(assoc_class, criteria, seen)
|
164
64
|
elsif deleting?(reflection)
|
@@ -166,40 +66,41 @@ module DeleteRecursively
|
|
166
66
|
end
|
167
67
|
end
|
168
68
|
end
|
69
|
+
|
169
70
|
delete_with_applicable_criteria(record_class, criteria)
|
170
71
|
end
|
171
72
|
|
73
|
+
def enabled_for?(reflection)
|
74
|
+
reflection.options[:dependent] == NEW_DEPENDENT_OPTION
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
172
79
|
def delete_with_applicable_criteria(record_class, criteria)
|
173
80
|
applicable_criteria = criteria.select do |column_name, _value|
|
174
81
|
record_class.column_names.include?(column_name.to_s)
|
175
82
|
end
|
176
83
|
record_class.where(applicable_criteria).delete_all
|
177
84
|
end
|
178
|
-
end
|
179
|
-
end
|
180
|
-
|
181
|
-
require 'active_record'
|
182
85
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
assoc_builder = Builder.const_get(assoc_name)
|
187
|
-
assoc_builder.singleton_class.prepend(DeleteRecursively::OptionPermission)
|
86
|
+
def recurse_on?(reflection)
|
87
|
+
enabled_for?(reflection) || destructive?(reflection)
|
88
|
+
end
|
188
89
|
|
189
|
-
|
190
|
-
|
90
|
+
def destructive?(reflection)
|
91
|
+
%i[destroy destroy_all].include?(reflection.options[:dependent])
|
191
92
|
end
|
192
|
-
end
|
193
93
|
|
194
|
-
|
195
|
-
|
196
|
-
DeleteRecursively.delete_records_recursively(self.class, id, force: force)
|
94
|
+
def deleting?(reflection)
|
95
|
+
[:delete, :delete_all, NEW_DEPENDENT_OPTION].include?(reflection.options[:dependent])
|
197
96
|
end
|
198
|
-
end
|
199
97
|
|
200
|
-
|
201
|
-
|
202
|
-
|
98
|
+
def destructive_method(reflection, force: false)
|
99
|
+
if deleting?(reflection) || force && destructive?(reflection)
|
100
|
+
:delete
|
101
|
+
elsif destructive?(reflection)
|
102
|
+
:destroy
|
103
|
+
end
|
203
104
|
end
|
204
105
|
end
|
205
106
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: delete_recursively
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Janosch Müller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -36,14 +36,14 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version: '2.
|
39
|
+
version: '2.4'
|
40
40
|
type: :development
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version: '2.
|
46
|
+
version: '2.4'
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: codecov
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -142,6 +142,10 @@ extensions: []
|
|
142
142
|
extra_rdoc_files: []
|
143
143
|
files:
|
144
144
|
- lib/delete_recursively.rb
|
145
|
+
- lib/delete_recursively/active_record_extensions.rb
|
146
|
+
- lib/delete_recursively/associated_class_finder.rb
|
147
|
+
- lib/delete_recursively/dependent_id_finder.rb
|
148
|
+
- lib/delete_recursively/railtie.rb
|
145
149
|
- lib/delete_recursively/version.rb
|
146
150
|
homepage: https://github.com/jaynetics/delete_recursively
|
147
151
|
licenses:
|
@@ -162,7 +166,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
162
166
|
- !ruby/object:Gem::Version
|
163
167
|
version: '0'
|
164
168
|
requirements: []
|
165
|
-
rubygems_version: 3.4.
|
169
|
+
rubygems_version: 3.4.1
|
166
170
|
signing_key:
|
167
171
|
specification_version: 4
|
168
172
|
summary: Delete ActiveRecords efficiently
|