Skip to content

Commit 60481c0

Browse files
committed
MONGOID-5136 Fix touch with custom field on embedded documents. In addition:
- Perform all touch operations on the document in a single write operation (MONGOID-5142) - 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 much more readable
1 parent 16a40cf commit 60481c0

File tree

4 files changed

+80
-52
lines changed

4 files changed

+80
-52
lines changed

lib/mongoid/atomic.rb

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -367,29 +367,5 @@ def generate_atomic_updates(mods, doc)
367367
mods.add_to_set(doc.atomic_array_add_to_sets)
368368
mods.pull_all(doc.atomic_array_pulls)
369369
end
370-
371-
# Get the atomic updates for a touch operation. Should only include the
372-
# updated_at field and the optional extra field.
373-
#
374-
# @api private
375-
#
376-
# @example Get the touch atomic updates.
377-
# document.touch_atomic_updates
378-
#
379-
# @param [ Symbol ] field The optional field.
380-
#
381-
# @return [ Hash ] The atomic updates.
382-
#
383-
# @since 3.0.6
384-
def touch_atomic_updates(field = nil)
385-
updates = atomic_updates
386-
return {} unless atomic_updates.key?("$set")
387-
touches = {}
388-
updates["$set"].each_pair do |key, value|
389-
key_regex = /updated_at|u_at#{"|" + field if field.present?}/
390-
touches.merge!({ key => value }) if key =~ key_regex
391-
end
392-
{ "$set" => touches }
393-
end
394370
end
395371
end

lib/mongoid/touchable.rb

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,38 +25,70 @@ module InstanceMethods
2525
# @since 3.0.0
2626
def touch(field = nil)
2727
return false if _root.new_record?
28-
current = Time.now
29-
field = database_field_name(field)
30-
write_attribute(:updated_at, current) if respond_to?("updated_at=")
31-
write_attribute(field, current) if field
32-
33-
# If the document being touched is embedded, touch its parents
34-
# all the way through the composition hierarchy to the root object,
35-
# because when an embedded document is changed the write is actually
36-
# performed by the composition root. See MONGOID-3468.
37-
if _parent
38-
# This will persist updated_at on this document as well as parents.
39-
# TODO support passing the field name to the parent's touch method;
40-
# I believe it should be read out of
41-
# _association.inverse_association.options but inverse_association
42-
# seems to not always/ever be set here. See MONGOID-5014.
43-
_parent.touch
44-
else
45-
# If the current document is not embedded, it is composition root
46-
# and we need to persist the write here.
47-
touches = touch_atomic_updates(field)
48-
unless touches["$set"].blank?
49-
selector = atomic_selector
50-
_root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
51-
end
28+
29+
touches = __gather_touch_updates(Time.now, field)
30+
unless touches.blank?
31+
selector = _root.atomic_selector
32+
_root.collection.find(selector).update_one(positionally(selector, '$set' => touches), session: _session)
5233
end
5334

54-
# Callbacks are invoked on the composition root first and on the
55-
# leaf-most embedded document last.
56-
# TODO add tests, see MONGOID-5015.
57-
run_callbacks(:touch)
35+
__run_touch_callbacks_from_root
5836
true
5937
end
38+
39+
# Recursively sets touchable fields on the current document and each of its
40+
# parents, including the root node. Returns the combined atomic $set
41+
# operations to be performed on the root document.
42+
#
43+
# @param [ Time ] now The timestamp used for synchronizing the touched time.
44+
# @param [ Symbol ] field The name of an additional field to update.
45+
#
46+
# @return [ Hash<String, Time> ] The touch operations to perform as an atomic $set.
47+
#
48+
# @api private
49+
def __gather_touch_updates(now, field = nil)
50+
field = database_field_name(field)
51+
write_attribute(:updated_at, now) if respond_to?("updated_at=")
52+
write_attribute(field, now) if field
53+
54+
touches = __extract_touches_from_atomic_sets(field) || {}
55+
touches.merge!(_parent.__gather_touch_updates(now) || {}) if _parent
56+
touches
57+
end
58+
59+
# Recursively runs :touch callbacks for the document and its parents,
60+
# beginning with the root document and cascading through each successive
61+
# child document.
62+
#
63+
# @api private
64+
#
65+
# TODO add tests, see MONGOID-5015.
66+
def __run_touch_callbacks_from_root
67+
_parent.__run_touch_callbacks_from_root if _parent
68+
run_callbacks(:touch)
69+
end
70+
71+
# Extract and remove the atomic updates for the touch operation(s)
72+
# from the currently enqueued atomic $set operations.
73+
#
74+
# @api private
75+
#
76+
# @param [ Symbol ] field The optional field.
77+
#
78+
# @return [ Hash ] The field-value pairs to update atomically.
79+
def __extract_touches_from_atomic_sets(field = nil)
80+
updates = atomic_updates['$set']
81+
return {} unless updates
82+
83+
touchable_keys = %w(updated_at u_at)
84+
touchable_keys << field.to_s if field.present?
85+
86+
updates.keys.each_with_object({}) do |key, touches|
87+
if touchable_keys.include?(key.split('.').last)
88+
touches[key] = updates.delete(key)
89+
end
90+
end
91+
end
6092
end
6193

6294
extend self

spec/mongoid/touchable_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,24 @@
132132
include_examples 'updates the child'
133133
include_examples 'updates the parent when :touch is true'
134134
include_examples 'updates the parent when :touch is not set'
135+
136+
context 'when also updating an additional field' do
137+
it 'persists the update to the additional field' do
138+
entrance
139+
update_time
140+
entrance.touch(:last_used_at)
141+
142+
entrance.reload
143+
building.reload
144+
145+
# This is the assertion we want.
146+
expect(entrance.last_used_at).to eq update_time
147+
148+
# Check other timestamps for good measure.
149+
expect(entrance.updated_at).to eq update_time
150+
expect(building.updated_at).to eq update_time
151+
end
152+
end
135153
end
136154

137155
context "when the document is referenced" do

spec/mongoid/touchable_spec_models.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class Entrance
1616
include Mongoid::Timestamps
1717

1818
embedded_in :building
19+
20+
field :last_used_at, type: Time
1921
end
2022

2123
class Floor

0 commit comments

Comments
 (0)