Skip to content

Commit 9ab75a8

Browse files
MONGOID-5217 Add _previously_was methods (#5195)
1 parent adfedd7 commit 9ab75a8

File tree

9 files changed

+293
-7
lines changed

9 files changed

+293
-7
lines changed

docs/release-notes/mongoid-8.0.txt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ Mongoid 8.0 behavior:
169169

170170
Band.any_of({name: 'Rolling Stone'}, {founded: 1990}).
171171
any_of({members: 2}, {last_tour: 1995})
172-
# =>
172+
# =>
173173
# #<Mongoid::Criteria
174174
# selector: {"$or"=>[{"name"=>"Rolling Stone"}, {"founded"=>1990}],
175175
# "$and"=>[{"$or"=>[{"members"=>2}, {"last_tour"=>1995}]}]}
@@ -178,7 +178,7 @@ Mongoid 8.0 behavior:
178178
# embedded: false>
179179

180180
Band.any_of({name: 'Rolling Stone'}, {founded: 1990}).any_of({members: 2})
181-
# =>
181+
# =>
182182
# #<Mongoid::Criteria
183183
# selector: {"$or"=>[{"name"=>"Rolling Stone"}, {"founded"=>1990}], "members"=>2}
184184
# options: {}
@@ -191,7 +191,7 @@ Mongoid 7 behavior:
191191

192192
Band.any_of({name: 'Rolling Stone'}, {founded: 1990}).
193193
any_of({members: 2}, {last_tour: 1995})
194-
# =>
194+
# =>
195195
# #<Mongoid::Criteria
196196
# selector: {"$or"=>[{"name"=>"Rolling Stone"}, {"founded"=>1990},
197197
# {"members"=>2}, {"last_tour"=>1995}]}
@@ -200,7 +200,7 @@ Mongoid 7 behavior:
200200
# embedded: false>
201201

202202
Band.any_of({name: 'Rolling Stone'}, {founded: 1990}).any_of({members: 2})
203-
# =>
203+
# =>
204204
# #<Mongoid::Criteria
205205
# selector: {"$or"=>[{"name"=>"Rolling Stone"}, {"founded"=>1990}], "members"=>2}
206206
# options: {}
@@ -248,3 +248,31 @@ Mongoid 7 output:
248248

249249
Notice that in 7 ``attribute_was(:age)`` returns the old attribute value,
250250
while in 8.0 ``attribute_was(:age)`` returns the new value.
251+
252+
``*_previously_was``, ``previously_new_record?``, and ``previously_persisted?`` helpers
253+
---------------------------------------------------------------------------------------
254+
255+
Mongoid 8.0 introduces ActiveModel-compatible ``*_previously_was`` helpers,
256+
as well as ActiveRecord-compatible ``previously_new_record?`` and
257+
``previously_persisted?`` helpers:
258+
259+
.. code-block:: ruby
260+
261+
class User
262+
include Mongoid::Document
263+
264+
field :name, type: String
265+
field :age, type: Integer
266+
end
267+
268+
user = User.create!(name: 'Sam', age: 18)
269+
user.previously_new_record? # => true
270+
271+
user.name = "Nick"
272+
user.save!
273+
user.name_previously_was # => "Sam"
274+
user.age_previously_was # => 18
275+
user.previously_new_record? # => false
276+
277+
user.destroy
278+
user.previously_persisted? # => true

lib/mongoid/changeable.rb

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def changes
6969
# person.move_changes
7070
def move_changes
7171
@previous_changes = changes
72+
@previous_attributes = attributes.dup
7273
Atomic::UPDATES.each do |update|
7374
send(update).clear
7475
end
@@ -133,6 +134,13 @@ def setters
133134

134135
private
135136

137+
# Get attributes of the document before the document was saved.
138+
#
139+
# @return [ Hash ] Previous attributes
140+
def previous_attributes
141+
@previous_attributes ||= {}
142+
end
143+
136144
# Get the old and new value for the provided attribute.
137145
#
138146
# @example Get the attribute change.
@@ -185,6 +193,25 @@ def attribute_was(attr)
185193
attribute_changed?(attr) ? changed_attributes[attr] : attributes[attr]
186194
end
187195

196+
# Get the previous attribute value that was changed
197+
# before the document was saved.
198+
#
199+
# It the document has not been saved yet, or was just loaded from database,
200+
# this method returns nil for all attributes.
201+
#
202+
# @param [ String ] attr The attribute name.
203+
#
204+
# @return [ Object | nil ] Attribute value before the document was saved,
205+
# or nil if the document has not been saved yet.
206+
def attribute_previously_was(attr)
207+
attr = database_field_name(attr)
208+
if previous_changes.key?(attr)
209+
previous_changes[attr].first
210+
else
211+
previous_attributes[attr]
212+
end
213+
end
214+
188215
# Flag an attribute as going to change.
189216
#
190217
# @example Flag the attribute.
@@ -291,7 +318,7 @@ def create_dirty_default_change_check(name, meth)
291318
end
292319
end
293320

294-
# Creates the dirty change previous value accessor.
321+
# Creates the dirty change previous value accessors.
295322
#
296323
# @example Create the accessor.
297324
# Model.create_dirty_previous_value_accessor("name", "alias")
@@ -303,6 +330,9 @@ def create_dirty_previous_value_accessor(name, meth)
303330
re_define_method("#{meth}_was") do
304331
attribute_was(name)
305332
end
333+
re_define_method("#{meth}_previously_was") do
334+
attribute_previously_was(name)
335+
end
306336
end
307337
end
308338

lib/mongoid/persistable/updatable.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ def prepare_update(options = {})
102102
_mongoid_run_child_callbacks(:save) do
103103
_mongoid_run_child_callbacks(:update) do
104104
result = yield(self)
105+
self.previously_new_record = false
105106
post_process_persist(result, options)
106107
true
107108
end

lib/mongoid/reloadable.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ def reload
2727
end
2828
@attributes = reloaded
2929
@attributes_before_type_cast = {}
30-
changed_attributes.clear
30+
@changed_attributes = {}
31+
@previous_changes = {}
32+
@previous_attributes = {}
33+
@previously_new_record = false
3134
reset_readonly
3235
apply_defaults
3336
reload_relations

lib/mongoid/stateful.rb

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@ module Mongoid
66
# document can transition through.
77
module Stateful
88

9-
attr_writer :destroyed, :flagged_for_destroy, :new_record
9+
attr_writer :destroyed, :flagged_for_destroy, :previously_new_record
10+
11+
def new_record=(new_value)
12+
@new_record ||= false
13+
if @new_record && !new_value
14+
@previously_new_record = true
15+
end
16+
@new_record = new_value
17+
end
1018

1119
# Returns true if the +Document+ has not been persisted to the database,
1220
# false if it has. This is determined by the variable @new_record
@@ -20,6 +28,15 @@ def new_record?
2028
@new_record ||= false
2129
end
2230

31+
# Returns true if this document was just created -- that is, prior to the last
32+
# save, the object didn't exist in the database and new_record? would have
33+
# returned true.
34+
#
35+
# @return [ true, false ] True if was just created, false if not.
36+
def previously_new_record?
37+
@previously_new_record ||= false
38+
end
39+
2340
# Checks if the document has been saved to the database. Returns false
2441
# if the document has been destroyed.
2542
#
@@ -31,6 +48,15 @@ def persisted?
3148
!new_record? && !destroyed?
3249
end
3350

51+
# Checks if the document was previously saved to the database
52+
# but now it has been deleted.
53+
#
54+
# @return [ true, false ] True if was persisted but now destroyed,
55+
# otherwise false.
56+
def previously_persisted?
57+
!new_record? && destroyed?
58+
end
59+
3460
# Returns whether or not the document has been flagged for deletion, but
3561
# not destroyed yet. Used for atomic pulls of child documents.
3662
#

spec/integration/callbacks_spec.rb

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,4 +360,91 @@ class CBIntSpecProfile
360360
expect(architect.after_remove_num_buildings).to eq(2)
361361
end
362362
end
363+
364+
context '_previously was methods in after_save callback' do
365+
let(:title) do
366+
"Title"
367+
end
368+
369+
let(:updated_title) do
370+
"Updated title"
371+
end
372+
373+
let(:age) do
374+
10
375+
end
376+
377+
it do
378+
class PreviouslyWasPerson
379+
include Mongoid::Document
380+
381+
field :title, type: String
382+
field :age, type: Integer
383+
384+
attr_reader :after_save_vals
385+
386+
set_callback :save, :after do |doc|
387+
@after_save_vals ||= []
388+
@after_save_vals << [doc.title_previously_was, doc.age_previously_was]
389+
end
390+
end
391+
392+
person = PreviouslyWasPerson.create!(title: title, age: age)
393+
person.title = updated_title
394+
person.save!
395+
expect(person.after_save_vals).to eq([
396+
# Field values are nil before create
397+
[nil, nil],
398+
[title, age]
399+
])
400+
end
401+
end
402+
403+
context 'previously_new_record? in after_save' do
404+
it do
405+
class PreviouslyNewRecordPerson
406+
include Mongoid::Document
407+
408+
field :title, type: String
409+
field :age, type: Integer
410+
411+
attr_reader :previously_new_record_value
412+
413+
set_callback :save, :after do |doc|
414+
@previously_new_record_value = doc.previously_new_record?
415+
end
416+
end
417+
418+
person = PreviouslyNewRecordPerson.create!(title: "title", age: 55)
419+
expect(person.previously_new_record_value).to be_truthy
420+
person.title = "New title"
421+
person.save!
422+
expect(person.previously_new_record_value).to be_falsey
423+
end
424+
end
425+
426+
context 'previously_persisted? in after_destroy' do
427+
it do
428+
class PreviouslyPersistedPerson
429+
include Mongoid::Document
430+
431+
field :title, type: String
432+
field :age, type: Integer
433+
434+
attr_reader :previously_persisted_value
435+
436+
set_callback :destroy, :after do |doc|
437+
@previously_persisted_value = doc.previously_persisted?
438+
end
439+
end
440+
441+
unsaved_person = PreviouslyPersistedPerson.new(title: "title", age: 55)
442+
unsaved_person.destroy
443+
expect(unsaved_person.previously_persisted_value).to be_falsey
444+
445+
saved_person = PreviouslyPersistedPerson.create(title: "title", age: 55)
446+
saved_person.destroy
447+
expect(saved_person.previously_persisted_value).to be_truthy
448+
end
449+
end
363450
end

spec/mongoid/changeable_spec.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,56 @@
555555
end
556556
end
557557

558+
describe '#attribute_previously_was' do
559+
let(:previous_title) do
560+
"Grand Poobah"
561+
end
562+
563+
let(:age) do
564+
10
565+
end
566+
567+
let(:person) do
568+
Person.create!(title: previous_title, age: age)
569+
end
570+
571+
let(:updated_title) do
572+
"Captain Obvious"
573+
end
574+
575+
before do
576+
person.title = updated_title
577+
person.save!
578+
end
579+
580+
context 'when attribute changed' do
581+
it "returns the old value" do
582+
expect(person.send(:attribute_previously_was, "title")).to eq(previous_title)
583+
end
584+
585+
it "allows access via (attribute)_was" do
586+
expect(person.title_previously_was).to eq(previous_title)
587+
end
588+
end
589+
590+
context 'when attribute did not change' do
591+
it "returns the same value" do
592+
expect(person.send(:attribute_previously_was, "age")).to eq(age)
593+
end
594+
595+
it "allows access via (attribute)_was" do
596+
expect(person.age_previously_was).to eq(age)
597+
end
598+
end
599+
600+
it 'clears after reload' do
601+
person.reload
602+
expect(person.title_previously_was).to be_nil
603+
expect(person.age_previously_was).to be_nil
604+
end
605+
606+
end
607+
558608
describe "#attribute_will_change!" do
559609

560610
let(:aliases) do

spec/mongoid/reloadable_spec.rb

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,5 +628,38 @@
628628
church.acolytes.first.name.should == 'Borg'
629629
end
630630
end
631+
632+
context 'when document has previous changes' do
633+
context 'when document was updated' do
634+
let(:person) do
635+
Person.create!(title: 'Sir')
636+
end
637+
638+
before do
639+
person.title = 'Madam'
640+
person.save!
641+
person.reload
642+
end
643+
644+
it "resets previous changes" do
645+
expect(person.title_previously_was).to be_nil
646+
expect(person).not_to be_previously_persisted
647+
end
648+
end
649+
650+
context 'when document was created' do
651+
let(:person) do
652+
Person.create!(title: 'Sir')
653+
end
654+
655+
before do
656+
person.reload
657+
end
658+
659+
it "resets previous changes" do
660+
expect(person).not_to be_previously_new_record
661+
end
662+
end
663+
end
631664
end
632665
end

0 commit comments

Comments
 (0)