@@ -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
0 commit comments