@@ -22,46 +22,69 @@ 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
+ 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
60
55
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
63
63
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
65
88
end
66
89
end
67
90
@@ -80,9 +103,14 @@ def define_touchable!(association)
80
103
name = association . name
81
104
method_name = define_relation_touch_method ( name , association )
82
105
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?
86
114
end
87
115
end
88
116
0 commit comments