@@ -22,46 +22,75 @@ module InstanceMethods
22
22
# @return [ true/false ] false if record is new_record otherwise true.
23
23
def touch ( field = nil )
24
24
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 )
26
44
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
60
56
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
63
66
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
65
94
end
66
95
end
67
96
@@ -80,9 +109,13 @@ def define_touchable!(association)
80
109
name = association . name
81
110
method_name = define_relation_touch_method ( name , association )
82
111
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
83
114
klass . after_save method_name
84
115
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?
86
119
end
87
120
end
88
121
0 commit comments