Skip to content

Commit ecf3842

Browse files
authored
MONGOID-4979 Add :touch option to model save method (#5397)
* MONGOID-4979 test set_created_at * MONGOID-4979 undo test * MONGOID-4979 add touch: false option * MONGOID-4979 use the timeless method instead * MONGOID-4979 add optimization and timeless tests * MONGOID-4979 fix docs * MONGOID-4979 add release note * MONGOID-4979 add crud docs
1 parent 5e41670 commit ecf3842

File tree

10 files changed

+309
-7
lines changed

10 files changed

+309
-7
lines changed

docs/reference/crud.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,11 @@ operations with examples.
145145
the save.*
146146

147147
*Pass* ``validate: false`` *option to bypass validations.*
148+
149+
*Pass* ``touch: false`` *option to ignore the updates to the updated_at
150+
field. If the document being save has not been previously persisted,
151+
this option is ignored and the created_at and updated_at fields will be
152+
updated with the current time.*
148153
-
149154
.. code-block:: ruby
150155

@@ -154,6 +159,7 @@ operations with examples.
154159
)
155160
person.save
156161
person.save(validate: false)
162+
person.save(touch: false)
157163

158164
person.first_name = "Christian Johan"
159165
person.save

docs/release-notes/mongoid-8.1.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,18 @@ with a bang (!) raise an error:
4242

4343
Band.none.first!
4444
# => raise Mongoid::Errors::DocumentNotFound
45+
46+
47+
Added ``:touch`` option to ``#save``
48+
------------------------------------
49+
50+
Support for the ``:touch`` option has been added to the ``#save`` and ``#save!``
51+
methods. When this option is false, the ``updated_at`` field on the saved
52+
document and all of it's embedded documents will not be updated with the
53+
current time. When this option is true or unset, the ``updated_at`` field will
54+
be updated with the current time.
55+
56+
If the document being saved is a new document (i.e. it has not yet been
57+
persisted to the database), then the ``:touch`` option will be ignored, and the
58+
``updated_at`` (and ``created_at``) fields will be updated with the current
59+
time.

lib/mongoid/interceptable.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ def run_callbacks(kind, with_children: true, &block)
134134
# Run the callbacks for embedded documents.
135135
#
136136
# @param [ Symbol ] kind The type of callback to execute.
137-
# @param [ Array<Document> ] children Children to exeute callbacks on. If
137+
# @param [ Array<Document> ] children Children to execute callbacks on. If
138138
# nil, callbacks will be executed on all cascadable children of
139139
# the document.
140140
#

lib/mongoid/persistable/savable.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ module Savable
1414
#
1515
# @param [ Hash ] options Options to pass to the save.
1616
#
17-
# @return [ true | false ] True is success, false if not.
17+
# @option options [ true | false ] :touch Whether or not the updated_at
18+
# attribute will be updated with the current time. When this option is
19+
# false, none of the embedded documents will be touched. This option is
20+
# ignored when saving a new document, and the created_at and updated_at
21+
# will be set to the current time.
22+
#
23+
# @return [ true | false ] True if success, false if not.
1824
def save(options = {})
1925
if new_record?
2026
!insert(options).new_record?
@@ -31,6 +37,12 @@ def save(options = {})
3137
#
3238
# @param [ Hash ] options Options to pass to the save.
3339
#
40+
# @option options [ true | false ] :touch Whether or not the updated_at
41+
# attribute will be updated with the current time. When this option is
42+
# false, none of the embedded documents will be touched.This option is
43+
# ignored when saving a new document, and the created_at and updated_at
44+
# will be set to the current time.
45+
#
3446
# @raise [ Errors::Validations ] If validation failed.
3547
# @raise [ Errors::Callback ] If a callback returns false.
3648
#

lib/mongoid/persistable/updatable.rb

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,21 @@ def init_atomic_updates
9191
#
9292
# @param [ Hash ] options The options.
9393
#
94+
# @option options [ true | false ] :touch Whether or not the updated_at
95+
# attribute will be updated with the current time.
96+
#
9497
# @return [ true | false ] The result of the update.
9598
def prepare_update(options = {})
9699
return false if performing_validations?(options) &&
97100
invalid?(options[:context] || :update)
98101
process_flagged_destroys
102+
update_children = cascadable_children(:update)
103+
process_touch_option(options, update_children)
99104
run_callbacks(:save, with_children: false) do
100105
run_callbacks(:update, with_children: false) do
101106
run_callbacks(:persist_parent, with_children: false) do
102107
_mongoid_run_child_callbacks(:save) do
103-
_mongoid_run_child_callbacks(:update) do
108+
_mongoid_run_child_callbacks(:update, children: update_children) do
104109
result = yield(self)
105110
self.previously_new_record = false
106111
post_process_persist(result, options)
@@ -160,6 +165,24 @@ def update_document(options = {})
160165
end
161166
end
162167
end
168+
169+
# If there is a touch option and it is false, this method will call the
170+
# timeless method so that the updated_at attribute is not updated. It
171+
# will call the timeless method on all of the cascadable children as
172+
# well. Note that timeless is cleared in the before_update callback.
173+
#
174+
# @param [ Hash ] options The options.
175+
# @param [ Array<Document> ] children The children that the :update
176+
# callbacks will be executed on.
177+
#
178+
# @option options [ true | false ] :touch Whether or not the updated_at
179+
# attribute will be updated with the current time.
180+
def process_touch_option(options, children)
181+
unless options.fetch(:touch, true)
182+
timeless
183+
children.each(&:timeless)
184+
end
185+
end
163186
end
164187
end
165188
end

spec/mongoid/timestamps_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
end
128128

129129
# This section of tests describes the behavior of the updated_at field for
130-
# different updates on associations, as outlined in PR #5219.
130+
# different updates on referenced associations, as outlined in PR #5219.
131131
describe "updated_at attribute" do
132132
let!(:start_time) { Timecop.freeze(Time.at(Time.now.to_i)) }
133133

spec/mongoid/touchable_spec.rb

Lines changed: 235 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
end
9090
end
9191

92-
shared_examples 'updates the parent when :touch is not set' do
92+
shared_examples 'updates the parent when :touch is false' do
9393
it 'does not update updated_at on parent' do
9494
entrance
9595
update_time
@@ -130,7 +130,7 @@
130130

131131
include_examples 'updates the child'
132132
include_examples 'updates the parent when :touch is true'
133-
include_examples 'updates the parent when :touch is not set'
133+
include_examples 'updates the parent when :touch is false'
134134

135135
context 'when also updating an additional field' do
136136
it 'persists the update to the additional field' do
@@ -701,4 +701,237 @@
701701
end
702702
end
703703
end
704+
705+
describe "when saving a document" do
706+
707+
let!(:start_time) { Timecop.freeze(Time.at(Time.now.to_i)) }
708+
709+
let(:update_time) do
710+
Timecop.freeze(Time.at(Time.now.to_i) + 2)
711+
end
712+
713+
after do
714+
Timecop.return
715+
end
716+
717+
context "when only using the root document" do
718+
719+
shared_examples "timeless is cleared" do
720+
it "clears the timeless option" do
721+
expect(doc.timeless?).to be false
722+
end
723+
end
724+
725+
shared_examples "touches the document" do
726+
it "touches the document" do
727+
expect(doc.created_at).to eq(start_time)
728+
expect(doc.updated_at).to eq(start_time)
729+
end
730+
end
731+
732+
shared_examples "updates the document" do
733+
it "updates the document" do
734+
expect(doc.created_at).to eq(start_time)
735+
expect(doc.updated_at).to eq(update_time)
736+
end
737+
end
738+
739+
let(:doc) { Dokument.new }
740+
741+
context "when saving a new document" do
742+
743+
context "when not passing a touch option" do
744+
745+
before do
746+
doc.save!
747+
end
748+
749+
include_examples "touches the document"
750+
include_examples "timeless is cleared"
751+
end
752+
753+
context "when passing touch: true" do
754+
755+
before do
756+
doc.save!(touch: true)
757+
end
758+
759+
include_examples "touches the document"
760+
include_examples "timeless is cleared"
761+
end
762+
763+
context "when passing touch: false" do
764+
765+
before do
766+
doc.save!(touch: false)
767+
end
768+
769+
include_examples "touches the document"
770+
include_examples "timeless is cleared"
771+
end
772+
end
773+
774+
context "when updating a document" do
775+
before do
776+
doc.save!
777+
doc.title = "title"
778+
update_time
779+
end
780+
781+
context "when not passing a touch option" do
782+
783+
before do
784+
doc.save!
785+
end
786+
787+
include_examples "updates the document"
788+
include_examples "timeless is cleared"
789+
end
790+
791+
context "when passing touch: true" do
792+
793+
before do
794+
doc.save!(touch: true)
795+
end
796+
797+
include_examples "updates the document"
798+
include_examples "timeless is cleared"
799+
end
800+
801+
context "when passing touch: false" do
802+
803+
before do
804+
doc.save!(touch: false)
805+
end
806+
807+
include_examples "touches the document"
808+
include_examples "timeless is cleared"
809+
end
810+
end
811+
end
812+
813+
context "when saving embedded associations with cascadable callbacks" do
814+
815+
shared_examples "timeless is cleared" do
816+
it "clears the timeless option" do
817+
expect(book.timeless?).to be false
818+
expect(book.covers.first.timeless?).to be false
819+
end
820+
end
821+
822+
shared_examples "touches the document" do
823+
it "touches the document" do
824+
expect(book.created_at).to eq(start_time)
825+
expect(book.updated_at).to eq(start_time)
826+
end
827+
end
828+
829+
shared_examples "updates the document" do
830+
it "updates the document" do
831+
expect(book.created_at).to eq(start_time)
832+
expect(book.updated_at).to eq(update_time)
833+
end
834+
end
835+
836+
shared_examples "touches the children" do
837+
it "touches the children" do
838+
expect(book.covers.first.created_at).to eq(start_time)
839+
expect(book.covers.first.updated_at).to eq(start_time)
840+
end
841+
end
842+
843+
shared_examples "updates the children" do
844+
it "updates the children" do
845+
expect(book.covers.first.created_at).to eq(start_time)
846+
expect(book.covers.first.updated_at).to eq(update_time)
847+
end
848+
end
849+
850+
let(:book) do
851+
Book.new(covers: [ cover ])
852+
end
853+
854+
let(:cover) do
855+
Cover.new
856+
end
857+
858+
context "when saving a new document" do
859+
860+
context "when not passing a touch option" do
861+
862+
before do
863+
book.save!
864+
end
865+
866+
include_examples "touches the document"
867+
include_examples "touches the children"
868+
include_examples "timeless is cleared"
869+
end
870+
871+
context "when passing touch: true" do
872+
873+
before do
874+
book.save!(touch: true)
875+
end
876+
877+
include_examples "touches the document"
878+
include_examples "touches the children"
879+
include_examples "timeless is cleared"
880+
end
881+
882+
context "when passing touch: false" do
883+
884+
before do
885+
book.save!(touch: false)
886+
end
887+
888+
include_examples "touches the document"
889+
include_examples "touches the children"
890+
include_examples "timeless is cleared"
891+
end
892+
end
893+
894+
context "when updating a document" do
895+
before do
896+
book.save!
897+
book.title = "title"
898+
book.covers.first.title = "title"
899+
update_time
900+
end
901+
902+
context "when not passing a touch option" do
903+
904+
before do
905+
book.save!
906+
end
907+
908+
include_examples "updates the document"
909+
include_examples "updates the children"
910+
include_examples "timeless is cleared"
911+
end
912+
913+
context "when passing touch: true" do
914+
915+
before do
916+
book.save!(touch: true)
917+
end
918+
919+
include_examples "updates the document"
920+
include_examples "updates the children"
921+
include_examples "timeless is cleared"
922+
end
923+
924+
context "when passing touch: false" do
925+
926+
before do
927+
book.save!(touch: false)
928+
end
929+
930+
include_examples "touches the document"
931+
include_examples "touches the children"
932+
include_examples "timeless is cleared"
933+
end
934+
end
935+
end
936+
end
704937
end

0 commit comments

Comments
 (0)