Skip to content

Commit 46c0aa9

Browse files
authored
MONGOID-4838 safely use #clone method on embedded children with fields changed/removed (#5299)
* MONGOID-4834 add embeds_one tests * MONGOID-4838 add embeds_many tests * MONGOID-4838 embeds_one/many both work * MONGOID-4838 clean up and embedded_in test * MONGOID-4834 remove comment * MONGOID-4838 add docs and change to class method
1 parent cda13a5 commit 46c0aa9

File tree

3 files changed

+505
-9
lines changed

3 files changed

+505
-9
lines changed

lib/mongoid/association/relatable.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,28 @@ def parent_inclusions
306306
@parent_inclusions ||= []
307307
end
308308

309+
# Is this association an embeds_many or has_many association?
310+
#
311+
# @return [ true | false ] true if it is a *_many association, false if not.
312+
def many?
313+
[Referenced::HasMany, Embedded::EmbedsMany].any? { |a| self.is_a?(a) }
314+
end
315+
316+
# Is this association an embeds_one or has_one association?
317+
#
318+
# @return [ true | false ] true if it is a *_one association, false if not.
319+
def one?
320+
[Referenced::HasOne, Embedded::EmbedsOne].any? { |a| self.is_a?(a) }
321+
end
322+
323+
# Is this association an embedded_in or belongs_to association?
324+
#
325+
# @return [ true | false ] true if it is an embedded_in or belongs_to
326+
# association, false if not.
327+
def in_to?
328+
[Referenced::BelongsTo, Embedded::EmbeddedIn].any? { |a| self.is_a?(a) }
329+
end
330+
309331
private
310332

311333
# Gets the model classes with inverse associations of this model. This is used to determine

lib/mongoid/copyable.rb

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ module Copyable
1010
# the exception of the document's id, and will reset all the
1111
# instance variables.
1212
#
13-
# This clone also includes embedded documents.
13+
# This clone also includes embedded documents. If there is an _id field in
14+
# the embedded document, it will be maintained, unlike the root's _id.
15+
#
16+
# If cloning an embedded child, the embedded parent is not cloned and the
17+
# embedded_in association is not set.
1418
#
1519
# @example Clone the document.
1620
# document.clone
@@ -19,29 +23,49 @@ module Copyable
1923
def clone
2024
# @note This next line is here to address #2704, even though having an
2125
# _id and id field in the document would cause problems with Mongoid
22-
# elsewhere.
26+
# elsewhere. Note this is only done on the root document as we want
27+
# to maintain the same _id on the embedded documents.
2328
attrs = clone_document.except(*self.class.id_fields)
29+
Copyable.clone_with_hash(self.class, attrs)
30+
end
31+
alias :dup :clone
32+
33+
private
34+
35+
# Create clone of a document of the given klass with the given attributes
36+
# hash. This is used recursively so that embedded associations are cloned
37+
# safely.
38+
#
39+
# @param klass [ Class ] The class of the document to create.
40+
# @param attrs [ Hash ] The hash of the attributes.
41+
#
42+
# @return [ Document ] The new document.
43+
def self.clone_with_hash(klass, attrs)
2444
dynamic_attrs = {}
25-
_attribute_names = self.attribute_names
45+
_attribute_names = klass.attribute_names
2646
attrs.reject! do |attr_name, value|
2747
unless _attribute_names.include?(attr_name)
2848
dynamic_attrs[attr_name] = value
2949
true
3050
end
3151
end
32-
self.class.new(attrs).tap do |object|
52+
53+
Factory.build(klass, attrs).tap do |object|
3354
dynamic_attrs.each do |attr_name, value|
34-
if object.respond_to?("#{attr_name}=")
55+
assoc = object.embedded_relations[attr_name]
56+
if assoc&.one? && Hash === value
57+
object.send("#{attr_name}=", clone_with_hash(assoc.klass, value))
58+
elsif assoc&.many? && Array === value
59+
docs = value.map { |h| clone_with_hash(assoc.klass, h) }
60+
object.send("#{attr_name}=", docs)
61+
elsif object.respond_to?("#{attr_name}=")
3562
object.send("#{attr_name}=", value)
3663
else
3764
object.attributes[attr_name] = value
3865
end
3966
end
4067
end
4168
end
42-
alias :dup :clone
43-
44-
private
4569

4670
# Clone the document attributes
4771
#

0 commit comments

Comments
 (0)