@@ -22,46 +22,69 @@ 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+ write_attribute ( :updated_at , now ) if respond_to? ( "updated_at=" )
46+ write_attribute ( field , now ) if field
47+
48+ touches = __extract_touches_from_atomic_sets ( field ) || { }
49+
50+ # TODO: this needs to a guard `... if _parent && _association_to_parent.options[:touch]`
51+ # However, the `_association_to_parent` method doesn't exist!
52+ touches . merge! ( _parent . __gather_touch_updates ( now ) || { } ) if _parent
53+ touches
54+ end
6055
61- # Callbacks are invoked on the composition root first and on the
62- # leaf-most embedded document last.
56+ # Recursively runs :touch callbacks for the document and its parents,
57+ # beginning with the root document and cascading through each successive
58+ # child document.
59+ #
60+ # @api private
61+ def __run_touch_callbacks_from_root
62+ _parent . __run_touch_callbacks_from_root if _parent
6363 run_callbacks ( :touch )
64- true
64+ end
65+
66+ private
67+
68+ # Extract and remove the atomic updates for the touch operation(s)
69+ # from the currently enqueued atomic $set operations.
70+ #
71+ # @param [ Symbol ] field The optional field.
72+ #
73+ # @return [ Hash ] The field-value pairs to update atomically.
74+ #
75+ # @api private
76+ def __extract_touches_from_atomic_sets ( field = nil )
77+ updates = atomic_updates [ '$set' ]
78+ return { } unless updates
79+
80+ touchable_keys = %w( updated_at u_at )
81+ touchable_keys << field . to_s if field . present?
82+
83+ updates . keys . each_with_object ( { } ) do |key , touches |
84+ if touchable_keys . include? ( key . split ( '.' ) . last )
85+ touches [ key ] = updates . delete ( key )
86+ end
87+ end
6588 end
6689 end
6790
@@ -80,9 +103,14 @@ def define_touchable!(association)
80103 name = association . name
81104 method_name = define_relation_touch_method ( name , association )
82105 association . inverse_class . tap do |klass |
83- klass . after_save method_name
84- klass . after_destroy method_name
85- klass . after_touch method_name
106+ # TODO: for EMBEDDED docs, to ensure synchronized timestamps,
107+ # we should call .touch within the save/destroy
108+ # action rather than as a callback
109+ klass . after_save ( method_name )
110+ klass . after_destroy ( method_name )
111+
112+ # Embedded docs recursively handle touch updates within the #touch method itself
113+ klass . after_touch ( method_name ) unless association . embedded?
86114 end
87115 end
88116
0 commit comments