Skip to content

Commit 55e8aa5

Browse files
p-mongop
andauthored
MONGOID-3468 Always touch parents of embedded documents when embedded documents are touched (#4904)
* perform exact time assertions in existing tests * tests for child -> parent updated_at updates * propagate touches through composition hierarchy * fix the tests to reflect embedded associations always touching their parents * documentation * forgot to include Co-authored-by: Oleg Pudeyev <[email protected]>
1 parent a3d171a commit 55e8aa5

19 files changed

+239
-30
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ group :development do
1212
end
1313

1414
group :test do
15+
gem 'timecop'
1516
gem 'rspec-retry'
1617
gem 'benchmark-ips'
1718
gem 'rspec-core', '~> 3.7'

docs/tutorials/mongoid-relations.txt

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,19 +1082,54 @@ Mongoid to instantiate a new document when the association is accessed and it is
10821082
Touching
10831083
--------
10841084

1085-
Any ``belongs_to`` association, no matter where it hangs off from, can take an optional ``:touch``
1086-
option which will call the touch method on it and any parent associations with the option defined
1087-
when the base document calls ``#touch``.
1085+
Any ``belongs_to`` association can take an optional ``:touch`` option which
1086+
will cause the parent document be touched whenever the child document is
1087+
touched:
10881088

10891089
.. code-block:: ruby
10901090

1091-
class Band
1092-
include Mongoid::Document
1093-
belongs_to :label, touch: true
1094-
end
1091+
class Band
1092+
include Mongoid::Document
1093+
belongs_to :label, touch: true
1094+
end
10951095

1096-
band = Band.first
1097-
band.touch # Calls touch on the parent label.
1096+
band = Band.first
1097+
band.touch # Calls touch on the parent label.
1098+
1099+
``:touch`` can also take a string or symbol argument specifying a field to
1100+
be touched on the parent association in addition to updated_at:
1101+
1102+
.. code-block:: ruby
1103+
1104+
class Label
1105+
include Mongoid::Document
1106+
include Mongoid::Timestamps
1107+
field :bands_updated_at, type: Time
1108+
has_many :bands
1109+
end
1110+
1111+
class Band
1112+
include Mongoid::Document
1113+
belongs_to :label, touch: :bands_updated_at
1114+
end
1115+
1116+
label = Label.create!
1117+
band = Band.create!(label: label)
1118+
1119+
band.touch # Updates updated_at and bands_updated_at on the label.
1120+
1121+
When an embedded document is touched, its parents are recursively touched
1122+
through the composition root (because all of the parents are necessarily saved
1123+
when the embedded document is saved). The ``:touch`` attribute therefore is
1124+
unnecessary on ``embedded_in`` associations.
1125+
1126+
Mongoid currently `does not support specifying an additional field to be
1127+
touched on an embedded_in association <https://jira.mongodb.org/browse/MONGOID-5014>`_.
1128+
1129+
``:touch`` should not be set to ``false`` on an ``embedded_in`` association,
1130+
since composition hierarchy is always updated upon a touch of an embedded
1131+
document. This is currently not enforced but enforcement is `intended in the
1132+
future <https://jira.mongodb.org/browse/MONGOID-5016>`_.
10981133

10991134
The counter_cache Option
11001135
------------------------

gemfiles/driver_master.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ group :development do
1414
end
1515

1616
group :test do
17+
gem 'timecop'
1718
gem 'rspec-retry'
1819
gem 'benchmark-ips'
1920
gem 'rspec-core', '~> 3.7'

gemfiles/driver_master_jruby.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ group :development do
1818
end
1919

2020
group :test do
21+
gem 'timecop'
2122
gem 'rspec-retry'
2223
gem 'benchmark-ips'
2324
gem 'rspec-core', '~> 3.7'

gemfiles/driver_min.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ group :development do
1313
end
1414

1515
group :test do
16+
gem 'timecop'
1617
gem 'rspec-retry'
1718
gem 'benchmark-ips'
1819
gem 'rspec-core', '~> 3.7'

gemfiles/driver_min_jruby.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ group :development do
1616
end
1717

1818
group :test do
19+
gem 'timecop'
1920
gem 'rspec-retry'
2021
gem 'benchmark-ips'
2122
gem 'rspec-core', '~> 3.7'

gemfiles/driver_oldstable.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ group :development do
1414
end
1515

1616
group :test do
17+
gem 'timecop'
1718
gem 'rspec-retry'
1819
gem 'benchmark-ips'
1920
gem 'rspec-core', '~> 3.7'

gemfiles/driver_oldstable_jruby.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ group :development do
1616
end
1717

1818
group :test do
19+
gem 'timecop'
1920
gem 'rspec-retry'
2021
gem 'benchmark-ips'
2122
gem 'rspec-core', '~> 3.7'

gemfiles/driver_stable.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ group :development do
1414
end
1515

1616
group :test do
17+
gem 'timecop'
1718
gem 'rspec-retry'
1819
gem 'benchmark-ips'
1920
gem 'rspec-core', '~> 3.7'

gemfiles/driver_stable_jruby.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ group :development do
1616
end
1717

1818
group :test do
19+
gem 'timecop'
1920
gem 'rspec-retry'
2021
gem 'benchmark-ips'
2122
gem 'rspec-core', '~> 3.7'

gemfiles/i18n-1.0.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ group :development do
1212
end
1313

1414
group :test do
15+
gem 'timecop'
1516
gem 'rspec-retry'
1617
gem 'benchmark-ips'
1718
gem 'rspec-core', '~> 3.7'

gemfiles/rails_51.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ group :development do
1111
end
1212

1313
group :test do
14+
gem 'timecop'
1415
gem 'rspec-retry'
1516
gem 'benchmark-ips'
1617
gem 'rspec-core', '~> 3.7'

gemfiles/rails_52.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ group :development do
1111
end
1212

1313
group :test do
14+
gem 'timecop'
1415
gem 'rspec-retry'
1516
gem 'benchmark-ips'
1617
gem 'rspec-core', '~> 3.7'

gemfiles/rails_master.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ group :development do
1111
end
1212

1313
group :test do
14+
gem 'timecop'
1415
gem 'rspec-retry'
1516
gem 'benchmark-ips'
1617
gem 'rspec-core', '~> 3.7'

lib/mongoid/association/embedded/embedded_in.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class EmbeddedIn
2626
:autobuild,
2727
:cyclic,
2828
:polymorphic,
29-
:touch
29+
:touch,
3030
].freeze
3131

3232
# The complete list of valid options for this association, including

lib/mongoid/touchable.rb

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,30 @@ def touch(field = nil)
3030
write_attribute(:updated_at, current) if respond_to?("updated_at=")
3131
write_attribute(field, current) if field
3232

33-
touches = touch_atomic_updates(field)
34-
unless touches["$set"].blank?
35-
selector = atomic_selector
36-
_root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
33+
# If the document being touched is embedded, touch its parents
34+
# all the way through the composition hierarchy to the root object,
35+
# because when an embedded document is changed the write is actually
36+
# performed by the composition root. See MONGOID-3468.
37+
if _parent
38+
# This will persist updated_at on this document as well as parents.
39+
# TODO support passing the field name to the parent's touch method;
40+
# I believe it should be read out of
41+
# _association.inverse_association.options but inverse_association
42+
# seems to not always/ever be set here. See MONGOID-5014.
43+
_parent.touch
44+
else
45+
# If the current document is not embedded, it is composition root
46+
# and we need to persist the write here.
47+
touches = touch_atomic_updates(field)
48+
unless touches["$set"].blank?
49+
selector = atomic_selector
50+
_root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
51+
end
3752
end
53+
54+
# Callbacks are invoked on the composition root first and on the
55+
# leaf-most embedded document last.
56+
# TODO add tests, see MONGOID-5015.
3857
run_callbacks(:touch)
3958
true
4059
end

spec/lite_spec_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
# https://github.com/jruby/jruby/issues/5599
1616
require 'pp'
1717

18+
autoload :Timecop, 'timecop'
19+
1820
require 'support/spec_config'
1921
require 'mrss/lite_constraints'
2022
require "support/session_registry"

spec/mongoid/touchable_spec.rb

Lines changed: 104 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
# encoding: utf-8
33

44
require "spec_helper"
5+
require_relative './touchable_spec_models'
56

67
describe Mongoid::Touchable do
78

89
describe "#touch" do
910

1011
context "when the document has no associations" do
1112
let(:updatable) do
12-
Updatable.create
13+
Updatable.create!
1314
end
1415

1516
it "responds to #touch" do
@@ -28,30 +29,117 @@
2829
end
2930
end
3031

31-
context "when the document is embedded" do
32+
context 'when the document has a parent association' do
3233

33-
before do
34-
Label.send(:include, Mongoid::Touchable::InstanceMethods)
34+
let(:building) do
35+
parent_cls.create!
3536
end
3637

37-
let(:band) do
38-
Band.create(name: "Placebo")
38+
let(:entrance) do
39+
building.entrances.create!
3940
end
4041

41-
let(:label) do
42-
band.create_label(name: "Mute", updated_at: 10.days.ago)
42+
let(:floor) do
43+
building.floors.create!
4344
end
4445

45-
before do
46-
label.touch
46+
let!(:start_time) { Timecop.freeze(Time.at(Time.now.to_i)) }
47+
48+
let(:update_time) do
49+
Timecop.freeze(Time.at(Time.now.to_i) + 2)
4750
end
4851

49-
it "updates the updated_at timestamp" do
50-
expect(label.updated_at).to be_within(1).of(Time.now)
52+
after do
53+
Timecop.return
54+
end
55+
56+
shared_examples 'updates the child' do
57+
it "updates the updated_at timestamp" do
58+
entrance
59+
update_time
60+
entrance.touch
61+
62+
entrance.updated_at.should == update_time
63+
end
64+
65+
it "persists the changes" do
66+
entrance
67+
update_time
68+
entrance.touch
69+
70+
entrance.reload.updated_at.should == update_time
71+
end
5172
end
5273

53-
it "persists the changes" do
54-
expect(label.reload.updated_at).to be_within(1).of(Time.now)
74+
shared_examples 'updates the parent when :touch is true' do
75+
76+
it 'updates updated_at on parent' do
77+
floor
78+
update_time
79+
floor.touch
80+
81+
building.updated_at.should == update_time
82+
end
83+
84+
it 'persists updated updated_at on parent' do
85+
floor
86+
update_time
87+
floor.touch
88+
89+
building.reload.updated_at.should == update_time
90+
end
91+
end
92+
93+
shared_examples 'updates the parent when :touch is not set' do
94+
it 'does not update updated_at on parent' do
95+
entrance
96+
update_time
97+
entrance.touch
98+
99+
building.updated_at.should == update_time
100+
end
101+
102+
it 'does not persist updated updated_at on parent' do
103+
entrance
104+
update_time
105+
entrance.touch
106+
107+
building.reload.updated_at.should == update_time
108+
end
109+
end
110+
111+
shared_examples 'does not update the parent when :touch is not set' do
112+
it 'does not update updated_at on parent' do
113+
entrance
114+
update_time
115+
entrance.touch
116+
117+
building.updated_at.should == start_time
118+
end
119+
120+
it 'does not persist updated updated_at on parent' do
121+
entrance
122+
update_time
123+
entrance.touch
124+
125+
building.reload.updated_at.should == start_time
126+
end
127+
end
128+
129+
context "when the document is embedded" do
130+
let(:parent_cls) { TouchableSpec::Embedded::Building }
131+
132+
include_examples 'updates the child'
133+
include_examples 'updates the parent when :touch is true'
134+
include_examples 'updates the parent when :touch is not set'
135+
end
136+
137+
context "when the document is referenced" do
138+
let(:parent_cls) { TouchableSpec::Referenced::Building }
139+
140+
include_examples 'updates the child'
141+
include_examples 'updates the parent when :touch is true'
142+
include_examples 'does not update the parent when :touch is not set'
55143
end
56144
end
57145

@@ -415,11 +503,11 @@
415503
context "when modifying the child" do
416504

417505
let!(:agency) do
418-
Agency.create
506+
Agency.create!
419507
end
420508

421509
let!(:agent) do
422-
agency.agents.create(number: '1')
510+
agency.agents.create!(number: '1')
423511
end
424512

425513
it "updates the parent's updated at" do

0 commit comments

Comments
 (0)