Skip to content

Commit 06221c5

Browse files
MONGOID-5504 Clear changes after :touch (#5534)
1 parent 31cd9ba commit 06221c5

File tree

3 files changed

+140
-7
lines changed

3 files changed

+140
-7
lines changed

docs/release-notes/mongoid-9.0.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ The complete list of releases is available `on GitHub
1717
please consult GitHub releases for detailed release notes and JIRA for
1818
the complete list of issues fixed in each release, including bug fixes.
1919

20+
``touch`` method now clears changed state
21+
-----------------------------------------
22+
23+
In Mongoid 8.x and older ``touch`` method leaves models in the changed state:
24+
25+
.. code-block:: ruby
26+
27+
# Mongoid 8.x behaviour
28+
band = Band.create!
29+
band.touch
30+
band.changed? # => true
31+
band.changes # => {"updated_at"=>[2023-01-30 13:12:57.477191135 UTC, 2023-01-30 13:13:11.482975646 UTC]}
32+
33+
Starting from 9.0 Mongoid now correctly clears changed state after using ``touch``
34+
method.
35+
36+
.. code-block:: ruby
37+
38+
# Mongoid 9.0 behaviour
39+
band = Band.create!
40+
band.touch
41+
band.changed? # => false
42+
band.changes # => {}
43+
2044
Sandbox Mode for Rails Console
2145
------------------------------
2246

lib/mongoid/touchable.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ module InstanceMethods
2323
def touch(field = nil)
2424
return false if _root.new_record?
2525

26-
touches = _gather_touch_updates(Time.configured.now, field)
27-
_root.send(:persist_atomic_operations, '$set' => touches) if touches.present?
26+
begin
27+
touches = _gather_touch_updates(Time.configured.now, field)
28+
_root.send(:persist_atomic_operations, '$set' => touches) if touches.present?
29+
_run_touch_callbacks_from_root
30+
ensure
31+
_clear_touch_updates(field)
32+
end
2833

29-
_run_touch_callbacks_from_root
3034
true
3135
end
3236

@@ -50,6 +54,17 @@ def _gather_touch_updates(now, field = nil)
5054
touches
5155
end
5256

57+
# Clears changes for the model caused by touch operation.
58+
#
59+
# @param [ Symbol ] field The name of an additional field to update.
60+
#
61+
# @api private
62+
def _clear_touch_updates(field = nil)
63+
remove_change(:updated_at)
64+
remove_change(field) if field
65+
_parent._clear_touch_updates if _touchable_parent?
66+
end
67+
5368
# Recursively runs :touch callbacks for the document and its parents,
5469
# beginning with the root document and cascading through each successive
5570
# child document.

spec/mongoid/touchable_spec.rb

Lines changed: 98 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@
2626
updatable.touch
2727
expect(updatable.updated_at).to be > updated_at
2828
end
29+
30+
it 'does not leave model in changed state' do
31+
updatable.touch
32+
expect(updatable).not_to be_changed
33+
end
2934
end
3035

3136
context 'when the document has a parent association' do
@@ -255,8 +260,8 @@
255260
expect(touched).to be true
256261
end
257262

258-
it "keeps changes for next callback" do
259-
expect(agent.changes).to_not be_empty
263+
it "clears changes" do
264+
expect(agent.changes).to be_empty
260265
end
261266
end
262267

@@ -290,8 +295,8 @@
290295
expect(touched).to be true
291296
end
292297

293-
it "keeps changes for next callback" do
294-
expect(agent.changes).to_not be_empty
298+
it "clears changes" do
299+
expect(agent.changes).to be_empty
295300
end
296301
end
297302
end
@@ -1320,4 +1325,93 @@
13201325
end
13211326
end
13221327
end
1328+
1329+
context 'when updated after touch' do
1330+
let(:touch_time) { Timecop.freeze(Time.at(Time.now.to_i) + 2) }
1331+
1332+
let(:update_time) { Timecop.freeze(Time.at(Time.now.to_i) + 4) }
1333+
1334+
let!(:book) { Book.create! }
1335+
1336+
after do
1337+
Timecop.return
1338+
end
1339+
1340+
it 'updates updated_at' do
1341+
touch_time
1342+
book.touch
1343+
update_time
1344+
book.title = 'This book has no name'
1345+
book.save!
1346+
expect(book.updated_at).to eq(update_time)
1347+
end
1348+
end
1349+
1350+
context 'callbacks' do
1351+
class TouchableParent
1352+
include Mongoid::Document
1353+
include Mongoid::Timestamps
1354+
1355+
attr_reader :before_touch_called, :after_touch_called
1356+
1357+
set_callback(:touch, :before) do
1358+
@before_touch_called = true
1359+
end
1360+
1361+
set_callback(:touch, :after) do
1362+
@after_touch_called = true
1363+
end
1364+
1365+
embeds_one :child, inverse_of: :parent, class_name: 'TouchableChild'
1366+
end
1367+
1368+
class TouchableChild
1369+
include Mongoid::Document
1370+
include Mongoid::Timestamps
1371+
1372+
attr_reader :before_touch_called, :after_touch_called
1373+
1374+
set_callback(:touch, :before) do
1375+
@before_touch_called = true
1376+
end
1377+
1378+
set_callback(:touch, :after) do
1379+
@after_touch_called = true
1380+
end
1381+
1382+
embedded_in :parent, inverse_of: :child, class_name: 'TouchableParent', touch: true
1383+
end
1384+
1385+
let(:parent) do
1386+
TouchableParent.create!.tap do |parent|
1387+
parent.child = TouchableChild.create!(parent: parent)
1388+
end
1389+
end
1390+
1391+
let(:child) do
1392+
parent.child
1393+
end
1394+
1395+
it 'calls touch callbacks on parent' do
1396+
parent.touch
1397+
expect(parent.before_touch_called).to eq(true)
1398+
expect(parent.after_touch_called).to eq(true)
1399+
end
1400+
1401+
context 'when touch is calles on a child' do
1402+
before do
1403+
child.touch
1404+
end
1405+
1406+
it 'calls touch callbacks on parent' do
1407+
expect(parent.before_touch_called).to eq(true)
1408+
expect(parent.after_touch_called).to eq(true)
1409+
end
1410+
1411+
it 'calls touch callbacks on child' do
1412+
expect(child.before_touch_called).to eq(true)
1413+
expect(child.after_touch_called).to eq(true)
1414+
end
1415+
end
1416+
end
13231417
end

0 commit comments

Comments
 (0)