Skip to content

Commit 604a407

Browse files
MONGOID-5274 / MONGOID-5142 - Rework #touch with embedded documents (#5045)
* MONGOID-5142 - Optimize #touch method: - Perform all touch operations on the document in a single write operation - Synchronize the timestamp of all touches on the document (parent+child will have same timestamp) - Remove touches from the atomic set operations, so that they are no longer there when saving the next time. - Cleanup/refactor touch method to be more readable * typo * WIP on fixes * all but one spec passing... * Fix final spec * Add missing code doc * Fix failing spec * Update options.rb * Use Set * Fix last commit * Remove unnecessary relation touch conditional * Use single underscores * Add additional multi-level specs * Update lib/mongoid/touchable.rb Co-authored-by: Neil Shweky <[email protected]> * MONGOID-5136 explicitly specify touch in models, and fix test bug * Add release note * Update docs/release-notes/mongoid-9.0.txt * MONGOID-5136 fix and format specs Co-authored-by: shields <[email protected]> Co-authored-by: Neil Shweky <[email protected]>
1 parent 7a4c3cd commit 604a407

File tree

7 files changed

+600
-140
lines changed

7 files changed

+600
-140
lines changed

docs/release-notes/mongoid-9.0.txt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,40 @@ persistence operations was already correctly using the
7575
``Mongoid.use_activesupport_time_zone`` setting.
7676

7777

78+
```#touch`` method on embedded documents correctly handles ``touch: false`` option
79+
----------------------------------------------------------------------------------
80+
81+
When the ``touch: false`` option is set on an ``embedded_in`` relation,
82+
calling the ``#touch`` method on an embedded child document will not
83+
invoke ``#touch`` on its parent document.
84+
85+
.. code-block:: ruby
86+
87+
class Address
88+
include Mongoid::Document
89+
include Mongoid::Timestamps
90+
91+
embedded_in :mall, touch: false
92+
end
93+
94+
class Mall
95+
include Mongoid::Document
96+
include Mongoid::Timestamps
97+
98+
embeds_many :addresses
99+
end
100+
101+
mall = Mall.create!
102+
address = mall.addresses.create!
103+
104+
address.touch
105+
#=> updates address.updated_at but not mall.updated_at
106+
107+
In addition, the ``#touch`` method has been optimized to perform one
108+
persistence operation per parent document, even when using multiple
109+
levels of nested embedded documents.
110+
111+
78112
Flipped default for ``:replace`` option in ``#upsert``
79113
------------------------------------------------------
80114

lib/mongoid/association/options.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,13 @@ def touch_field
112112
@touch_field ||= options[:touch] if (options[:touch].is_a?(String) || options[:touch].is_a?(Symbol))
113113
end
114114

115-
private
116-
115+
# Whether the association object should be automatically touched
116+
# when its inverse object is updated.
117+
#
118+
# @return [ true | false ] returns true if this association is
119+
# automatically touched, false otherwise. The default is false.
120+
#
121+
# @api private
117122
def touchable?
118123
!!@options[:touch]
119124
end

lib/mongoid/atomic.rb

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -328,51 +328,5 @@ def generate_atomic_updates(mods, doc)
328328
mods.add_to_set(doc.atomic_array_add_to_sets)
329329
mods.pull_all(doc.atomic_array_pulls)
330330
end
331-
332-
# Get the atomic updates for a touch operation. Should only include the
333-
# updated_at field and the optional extra field.
334-
#
335-
# @api private
336-
#
337-
# @example Get the touch atomic updates.
338-
# document.touch_atomic_updates
339-
#
340-
# @param [ Symbol ] field The optional field.
341-
#
342-
# @return [ Hash ] The atomic updates.
343-
def touch_atomic_updates(field = nil)
344-
updates = atomic_updates
345-
return {} unless atomic_updates.key?("$set")
346-
touches = {}
347-
wanted_keys = %w(updated_at u_at)
348-
# TODO this permits field to be passed as an empty string in which case
349-
# it is ignored, get rid of this behavior.
350-
if field.present?
351-
wanted_keys << field.to_s
352-
end
353-
updates["$set"].each_pair do |key, value|
354-
if wanted_keys.include?(key.split('.').last)
355-
touches.update(key => value)
356-
end
357-
end
358-
{ "$set" => touches }
359-
end
360-
361-
# Returns the $set atomic updates affecting the specified field.
362-
#
363-
# @param [ String ] field The field name.
364-
#
365-
# @api private
366-
def set_field_atomic_updates(field)
367-
updates = atomic_updates
368-
return {} unless atomic_updates.key?("$set")
369-
sets = {}
370-
updates["$set"].each_pair do |key, value|
371-
if key.split('.').last == field
372-
sets.update(key => value)
373-
end
374-
end
375-
{ "$set" => sets }
376-
end
377331
end
378332
end

lib/mongoid/touchable.rb

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,73 @@ module InstanceMethods
2222
# @return [ true/false ] false if document is new_record otherwise true.
2323
def touch(field = nil)
2424
return false if _root.new_record?
25-
current = Time.configured.now
25+
26+
touches = _gather_touch_updates(Time.configured.now, field)
27+
_root.send(:persist_atomic_operations, '$set' => touches) if touches.present?
28+
29+
_run_touch_callbacks_from_root
30+
true
31+
end
32+
33+
# Recursively sets touchable fields on the current document and each of its
34+
# parents, including the root node. Returns the combined atomic $set
35+
# operations to be performed on the root document.
36+
#
37+
# @param [ Time ] now The timestamp used for synchronizing the touched time.
38+
# @param [ Symbol ] field The name of an additional field to update.
39+
#
40+
# @return [ Hash<String, Time> ] The touch operations to perform as an atomic $set.
41+
#
42+
# @api private
43+
def _gather_touch_updates(now, field = nil)
2644
field = database_field_name(field)
27-
write_attribute(:updated_at, current) if respond_to?("updated_at=")
28-
write_attribute(field, current) if field
29-
30-
# If the document being touched is embedded, touch its parents
31-
# all the way through the composition hierarchy to the root object,
32-
# because when an embedded document is changed the write is actually
33-
# performed by the composition root. See MONGOID-3468.
34-
if _parent
35-
# This will persist updated_at on this document as well as parents.
36-
# TODO support passing the field name to the parent's touch method;
37-
# I believe it should be read out of
38-
# _association.inverse_association.options but inverse_association
39-
# seems to not always/ever be set here. See MONGOID-5014.
40-
_parent.touch
41-
42-
if field
43-
# If we are told to also touch a field, perform a separate write
44-
# for that field. See MONGOID-5136.
45-
# In theory we should combine the writes, which would require
46-
# passing the fields to be updated to the parents - MONGOID-5142.
47-
sets = set_field_atomic_updates(field)
48-
selector = atomic_selector
49-
_root.collection.find(selector).update_one(positionally(selector, sets), session: _session)
50-
end
51-
else
52-
# If the current document is not embedded, it is composition root
53-
# and we need to persist the write here.
54-
touches = touch_atomic_updates(field)
55-
unless touches["$set"].blank?
56-
selector = atomic_selector
57-
_root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
58-
end
59-
end
45+
write_attribute(:updated_at, now) if respond_to?("updated_at=")
46+
write_attribute(field, now) if field
6047

61-
# Callbacks are invoked on the composition root first and on the
62-
# leaf-most embedded document last.
48+
touches = _extract_touches_from_atomic_sets(field) || {}
49+
touches.merge!(_parent._gather_touch_updates(now) || {}) if _touchable_parent?
50+
touches
51+
end
52+
53+
# Recursively runs :touch callbacks for the document and its parents,
54+
# beginning with the root document and cascading through each successive
55+
# child document.
56+
#
57+
# @api private
58+
def _run_touch_callbacks_from_root
59+
_parent._run_touch_callbacks_from_root if _touchable_parent?
6360
run_callbacks(:touch)
64-
true
61+
end
62+
63+
# Indicates whether the parent exists and is touchable.
64+
#
65+
# @api private
66+
def _touchable_parent?
67+
_parent && _association&.inverse_association&.touchable?
68+
end
69+
70+
private
71+
72+
# Extract and remove the atomic updates for the touch operation(s)
73+
# from the currently enqueued atomic $set operations.
74+
#
75+
# @param [ Symbol ] field The optional field.
76+
#
77+
# @return [ Hash ] The field-value pairs to update atomically.
78+
#
79+
# @api private
80+
def _extract_touches_from_atomic_sets(field = nil)
81+
updates = atomic_updates['$set']
82+
return {} unless updates
83+
84+
touchable_keys = Set['updated_at', 'u_at']
85+
touchable_keys << field.to_s if field.present?
86+
87+
updates.keys.each_with_object({}) do |key, touches|
88+
if touchable_keys.include?(key.split('.').last)
89+
touches[key] = updates.delete(key)
90+
end
91+
end
6592
end
6693
end
6794

@@ -82,7 +109,10 @@ def define_touchable!(association)
82109
association.inverse_class.tap do |klass|
83110
klass.after_save method_name
84111
klass.after_destroy method_name
85-
klass.after_touch method_name
112+
113+
# Embedded docs handle touch updates recursively within
114+
# the #touch method itself
115+
klass.after_touch method_name unless association.embedded?
86116
end
87117
end
88118

@@ -114,13 +144,9 @@ def define_relation_touch_method(name, association)
114144
define_method(method_name) do
115145
without_autobuild do
116146
if relation = __send__(name)
117-
if association.touch_field
118-
# Note that this looks up touch_field at runtime, rather than
119-
# at method definition time.
120-
relation.touch association.touch_field
121-
else
122-
relation.touch
123-
end
147+
# This looks up touch_field at runtime, rather than at method definition time.
148+
# If touch_field is nil, it will only touch the default field (updated_at).
149+
relation.touch(association.touch_field)
124150
end
125151
end
126152
end

spec/integration/callbacks_models.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ class Star
2323
include Mongoid::Document
2424
include Mongoid::Timestamps
2525

26-
embedded_in :galaxy
26+
embedded_in :galaxy, touch: true
2727

2828
field :age, type: Integer
2929
field :was_touched_after_parent, type: Mongoid::Boolean, default: false
@@ -47,7 +47,7 @@ class Planet
4747
include Mongoid::Document
4848
include Mongoid::Timestamps
4949

50-
embedded_in :star
50+
embedded_in :star, touch: true
5151

5252
field :age, type: Integer
5353
field :was_touched_after_parent, type: Mongoid::Boolean, default: false

0 commit comments

Comments
 (0)