@@ -22,46 +22,73 @@ module InstanceMethods
22
22
# @return [ true/false ] false if document 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
+ write_attribute ( :updated_at , now ) if respond_to? ( "updated_at=" )
46
+ write_attribute ( field , now ) if field
60
47
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?
63
60
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
65
92
end
66
93
end
67
94
@@ -82,7 +109,10 @@ def define_touchable!(association)
82
109
association . inverse_class . tap do |klass |
83
110
klass . after_save method_name
84
111
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?
86
116
end
87
117
end
88
118
@@ -114,13 +144,9 @@ def define_relation_touch_method(name, association)
114
144
define_method ( method_name ) do
115
145
without_autobuild do
116
146
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 )
124
150
end
125
151
end
126
152
end
0 commit comments