Skip to content

Commit 604686f

Browse files
committed
Merge remote-tracking branch 'remotes/johnnyshields/MONGOID-5136-super-touch'
2 parents 8a9c638 + ce7dcb7 commit 604686f

File tree

3 files changed

+74
-87
lines changed

3 files changed

+74
-87
lines changed

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 & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,75 @@ module InstanceMethods
2222
# @return [ true/false ] false if record 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+
# TODO: we should check the type of field is time-like (date/time/datetime/bson-timestamp) before writing
46+
write_attribute(:updated_at, now) if respond_to?("updated_at=")
47+
write_attribute(field, now) if field
48+
49+
touches = __extract_touches_from_atomic_sets(field) || {}
50+
51+
# TODO: this needs to a guard `... if _parent && _association_to_parent.options[:touch]`
52+
# However, the `_association_to_parent` method doesn't exist!
53+
touches.merge!(_parent.__gather_touch_updates(now) || {}) if _parent
54+
touches
55+
end
6056

61-
# Callbacks are invoked on the composition root first and on the
62-
# leaf-most embedded document last.
57+
# Recursively runs :touch callbacks for the document and its parents,
58+
# beginning with the root document and cascading through each successive
59+
# child document.
60+
#
61+
# @api private
62+
#
63+
# TODO add tests, see MONGOID-5015.
64+
def __run_touch_callbacks_from_root
65+
_parent.__run_touch_callbacks_from_root if _parent
6366
run_callbacks(:touch)
64-
true
67+
end
68+
69+
private
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+
#
80+
# @api private
81+
def __extract_touches_from_atomic_sets(field = nil)
82+
updates = atomic_updates['$set']
83+
return {} unless updates
84+
85+
# TODO: this should evaluate the field alias of updated_at rather than assuming u_at
86+
touchable_keys = %w(updated_at u_at)
87+
touchable_keys << field.to_s if field.present?
88+
89+
updates.keys.each_with_object({}) do |key, touches|
90+
if touchable_keys.include?(key.split('.').last)
91+
touches[key] = updates.delete(key)
92+
end
93+
end
6594
end
6695
end
6796

@@ -80,9 +109,13 @@ def define_touchable!(association)
80109
name = association.name
81110
method_name = define_relation_touch_method(name, association)
82111
association.inverse_class.tap do |klass|
112+
# TODO: for EMBEDDED docs, save/destroy we should call .touch within the save/destroy
113+
# action rather than as a callback this will ensure synchronized timestamps
83114
klass.after_save method_name
84115
klass.after_destroy method_name
85-
klass.after_touch method_name
116+
117+
# Embedded docs recursively handle touch updates within the #touch method itself
118+
klass.after_touch method_name unless association.embedded?
86119
end
87120
end
88121

spec/mongoid/touchable_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@
142142
building.reload
143143

144144
# This is the assertion we want.
145-
entrance.last_used_at.should == update_time
145+
expect(entrance.last_used_at).to eq update_time
146146

147147
# Check other timestamps for good measure.
148-
entrance.updated_at.should == update_time
149-
building.updated_at.should == update_time
148+
expect(entrance.updated_at).to eq update_time
149+
expect(building.updated_at).to eq update_time
150150
end
151151
end
152152
end

0 commit comments

Comments
 (0)