Skip to content

Commit bb1dedf

Browse files
MONGOID-4528 Add more dirty methods (#5440)
Co-authored-by: Oleg Pudeyev <39304720+p-mongo@users.noreply.github.com>
1 parent a189655 commit bb1dedf

File tree

4 files changed

+374
-0
lines changed

4 files changed

+374
-0
lines changed

docs/release-notes/mongoid-8.1.txt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,74 @@ 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+
Added ``attribute_before_last_save``, ``saved_change_to_attribute``, ``saved_change_to_attribute?``, and ``will_save_change_to_attribute?`` methods
21+
----------------------------------------------------------------------------------------------------------------------------------------------------
22+
23+
These new methods behave identically to corresponding methods
24+
from ``ActiveRecord::AttributeMethods::Dirty``. The methods are particularly useful in
25+
callbacks:
26+
27+
.. code-block:: ruby
28+
29+
class Person
30+
include Mongoid::Document
31+
32+
field :name, type: String
33+
34+
before_save do
35+
puts "attribute_was(:name): #{attribute_was(:name)}"
36+
puts "attribute_before_last_save(:name): #{attribute_before_last_save(:name)}"
37+
puts "will_save_change_to_attribute?(:name): #{will_save_change_to_attribute?(:name)}"
38+
end
39+
40+
after_save do
41+
puts "attribute_was(:name): #{attribute_was(:name)}"
42+
puts "attribute_before_last_save(:name): #{attribute_before_last_save(:name)}"
43+
puts "saved_change_to_attribute(:name): #{saved_change_to_attribute(:name)}"
44+
puts "attribute_changed?(:name): #{attribute_changed?(:name)}"
45+
puts "saved_change_to_attribute?(:name): #{saved_change_to_attribute?(:name)}"
46+
end
47+
end
48+
49+
person = Person.create(name: 'John')
50+
#
51+
# before_save
52+
#
53+
## attribute_was(:name): nil
54+
## attribute_before_last_save(:name): nil
55+
## will_save_change_to_attribute?(:name): true
56+
#
57+
# after_save
58+
#
59+
## attribute_was(:name): John => New value
60+
## attribute_before_last_save(:name): nil => Value before save
61+
## saved_change_to_attribute(:name): [nil, "John"] => Both values
62+
## attribute_changed?(:name): false
63+
## saved_change_to_attribute?(:name): true => Correctly indicates that the change for :name was saved
64+
65+
person.name = 'Jane'
66+
person.save
67+
#
68+
# before_save
69+
#
70+
## attribute_was(:name): John => attribute_was not look back before the last save
71+
## attribute_before_last_save(:name): nil => value before the last save
72+
## will_save_change_to_attribute?(:name): true
73+
#
74+
# after_save
75+
#
76+
## attribute_was(:name): Jane => New value
77+
## attribute_before_last_save(:name): John => Value before save
78+
## saved_change_to_attribute(:name): ["John", "Jane"] => Both values
79+
## attribute_changed?(:name): false
80+
## saved_change_to_attribute?(:name): true => Correctly indicates that the change for :name was saved
81+
82+
For all of the new methods there are also shorter forms created dynamically, e.g.
83+
``attribute_before_last_save(:name)`` is equivalent to ``name_before_last_save``,
84+
``saved_change_to_attribute(:name)`` is equivalent to ``saved_change_to_name``,
85+
``saved_change_to_attribute?(:name)`` is equivalent to ``saved_change_to_name?``,
86+
and ``will_save_change_to_attribute?(:name)`` is equivalent to ``will_save_change_to_name?``.
87+
2088

2189
Configuration DSL No Longer Requires an Argument to its Block
2290
-------------------------------------------------------------

lib/mongoid/changeable.rb

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ def changes
6868
# @example Move the changes to previous.
6969
# person.move_changes
7070
def move_changes
71+
@changes_before_last_save = @previous_changes
7172
@previous_changes = changes
73+
@attributes_before_last_save = @previous_attributes
7274
@previous_attributes = attributes.dup
7375
Atomic::UPDATES.each do |update|
7476
send(update).clear
@@ -133,6 +135,72 @@ def setters
133135
mods
134136
end
135137

138+
# Returns the original value of an attribute before the last save.
139+
#
140+
# This method is useful in after callbacks to get the original value of
141+
# an attribute before the save that triggered the callbacks to run.
142+
#
143+
# @param [ Symbol | String ] attr The name of the attribute.
144+
#
145+
# @return [ Object ] Value of the attribute before the last save.
146+
def attribute_before_last_save(attr)
147+
attr = database_field_name(attr)
148+
attributes_before_last_save[attr]
149+
end
150+
151+
# Returns the change to an attribute during the last save.
152+
#
153+
# @param [ Symbol | String ] attr The name of the attribute.
154+
#
155+
# @return [ Array<Object> | nil ] If the attribute was changed, returns
156+
# an array containing the original value and the saved value, otherwise nil.
157+
def saved_change_to_attribute(attr)
158+
attr = database_field_name(attr)
159+
previous_changes[attr]
160+
end
161+
162+
# Returns whether this attribute changed during the last save.
163+
#
164+
# This method is useful in after callbacks, to see the change
165+
# in an attribute during the save that triggered the callbacks to run.
166+
#
167+
# @param [ String ] attr The name of the attribute.
168+
# @param **kwargs The optional keyword arguments.
169+
#
170+
# @option **kwargs [ Object ] :from The object the attribute was changed from.
171+
# @option **kwargs [ Object ] :to The object the attribute was changed to.
172+
#
173+
# @return [ true | false ] Whether the attribute has changed during the last save.
174+
def saved_change_to_attribute?(attr, **kwargs)
175+
changes = saved_change_to_attribute(attr)
176+
return false unless changes.is_a?(Array)
177+
if kwargs.key?(:from) && kwargs.key?(:to)
178+
changes.first == kwargs[:from] && changes.last == kwargs[:to]
179+
elsif kwargs.key?(:from)
180+
changes.first == kwargs[:from]
181+
elsif kwargs.key?(:to)
182+
changes.last == kwargs[:to]
183+
else
184+
true
185+
end
186+
end
187+
188+
# Returns whether this attribute change the next time we save.
189+
#
190+
# This method is useful in validations and before callbacks to determine
191+
# if the next call to save will change a particular attribute.
192+
#
193+
# @param [ String ] attr The name of the attribute.
194+
# @param **kwargs The optional keyword arguments.
195+
#
196+
# @option **kwargs [ Object ] :from The object the attribute was changed from.
197+
# @option **kwargs [ Object ] :to The object the attribute was changed to.
198+
#
199+
# @return [ true | false ] Whether the attribute change the next time we save.
200+
def will_save_change_to_attribute?(attr, **kwargs)
201+
attribute_changed?(attr, **kwargs)
202+
end
203+
136204
private
137205

138206
# Get attributes of the document before the document was saved.
@@ -142,6 +210,14 @@ def previous_attributes
142210
@previous_attributes ||= {}
143211
end
144212

213+
def changes_before_last_save
214+
@changes_before_last_save ||= {}
215+
end
216+
217+
def attributes_before_last_save
218+
@attributes_before_last_save ||= {}
219+
end
220+
145221
# Get the old and new value for the provided attribute.
146222
#
147223
# @example Get the attribute change.
@@ -317,6 +393,9 @@ def create_dirty_change_check(name, meth)
317393
re_define_method("#{meth}_changed?") do |**kwargs|
318394
attribute_changed?(name, **kwargs)
319395
end
396+
re_define_method("will_save_change_to_#{meth}?") do |**kwargs|
397+
will_save_change_to_attribute?(name, **kwargs)
398+
end
320399
end
321400
end
322401

@@ -350,6 +429,15 @@ def create_dirty_previous_value_accessor(name, meth)
350429
re_define_method("#{meth}_previously_was") do
351430
attribute_previously_was(name)
352431
end
432+
re_define_method("#{meth}_before_last_save") do
433+
attribute_before_last_save(name)
434+
end
435+
re_define_method("saved_change_to_#{meth}") do
436+
saved_change_to_attribute(name)
437+
end
438+
re_define_method("saved_change_to_#{meth}?") do |**kwargs|
439+
saved_change_to_attribute?(name, **kwargs)
440+
end
353441
end
354442
end
355443

spec/integration/callbacks_spec.rb

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,4 +447,111 @@ class PreviouslyPersistedPerson
447447
expect(saved_person.previously_persisted_value).to be_truthy
448448
end
449449
end
450+
451+
context 'saved_change_to_attribute, attribute_before_last_save, will_save_change_to_attribute' do
452+
class TestSCTAAndABLSInCallbacks
453+
include Mongoid::Document
454+
455+
field :name, type: String
456+
field :age, type: Integer
457+
458+
set_callback :save, :before do |doc|
459+
[:name, :age].each do |attr|
460+
saved_change_to_attribute_values_before[attr] += [saved_change_to_attribute(attr)]
461+
attribute_before_last_save_values_before[attr] += [attribute_before_last_save(attr)]
462+
will_save_change_to_attribute_values_before[attr] += [will_save_change_to_attribute?(attr)]
463+
end
464+
end
465+
466+
set_callback :save, :after do |doc|
467+
[:name, :age].each do |attr|
468+
saved_change_to_attribute_values_after[attr] += [saved_change_to_attribute(attr)]
469+
saved_change_to_attribute_q_values_after[attr] += [saved_change_to_attribute?(attr)]
470+
attribute_before_last_save_values_after[attr] += [attribute_before_last_save(attr)]
471+
end
472+
end
473+
474+
def saved_change_to_attribute_values_before
475+
@saved_change_to_attribute_values_before ||= Hash.new do
476+
[]
477+
end
478+
end
479+
480+
def attribute_before_last_save_values_before
481+
@attribute_before_last_save_values_before ||= Hash.new do
482+
[]
483+
end
484+
end
485+
486+
def saved_change_to_attribute_values_after
487+
@saved_change_to_attribute_values_after ||= Hash.new do
488+
[]
489+
end
490+
end
491+
492+
def saved_change_to_attribute_q_values_after
493+
@saved_change_to_attribute_q_values_after ||= Hash.new do
494+
[]
495+
end
496+
end
497+
498+
def attribute_before_last_save_values_after
499+
@attribute_before_last_save_values_after ||= Hash.new do
500+
[]
501+
end
502+
end
503+
504+
def will_save_change_to_attribute_values_before
505+
@will_save_change_to_attribute_values_before ||= Hash.new do
506+
[]
507+
end
508+
end
509+
end
510+
511+
it 'reproduces ActiveRecord::AttributeMethods::Dirty behavior' do
512+
subject = TestSCTAAndABLSInCallbacks.new(name: 'Name 1')
513+
subject.save!
514+
subject.age = 18
515+
subject.save!
516+
subject.name = 'Name 2'
517+
subject.save!
518+
519+
expect(subject.saved_change_to_attribute_values_before).to eq(
520+
{
521+
:name => [nil, [nil, "Name 1"], nil],
522+
:age => [nil, nil, [nil, 18]],
523+
}
524+
)
525+
expect(subject.saved_change_to_attribute_values_after).to eq(
526+
{
527+
:name => [[nil, "Name 1"], nil, ["Name 1", "Name 2"]],
528+
:age => [nil, [nil, 18], nil],
529+
}
530+
)
531+
expect(subject.saved_change_to_attribute_q_values_after).to eq(
532+
{
533+
:name => [true, false, true],
534+
:age => [false, true, false],
535+
}
536+
)
537+
expect(subject.attribute_before_last_save_values_before).to eq(
538+
{
539+
:name => [nil, nil, "Name 1"],
540+
:age => [nil, nil, nil]
541+
}
542+
)
543+
expect(subject.attribute_before_last_save_values_after).to eq(
544+
{
545+
:name => [nil, "Name 1", "Name 1"],
546+
:age => [nil, nil, 18]
547+
}
548+
)
549+
expect(subject.will_save_change_to_attribute_values_before).to eq(
550+
{
551+
:name => [true, false, true],
552+
:age => [false, true, false]
553+
}
554+
)
555+
end
556+
end
450557
end

0 commit comments

Comments
 (0)