I have an ActiveRecord class that looks something like this.
class Foo
belongs_to :bar, autosave: true
before_save :modify_bar
...
end
If I do some logging, I see that the bar
is being modified, but its changes are not saved. What's wrong?
The problem here is that autosave: true
simply sets up a normal before_save
callback, and before_save
callbacks are run in the order that they're created.**
Therefore, it tries to save the bar
, which has no changes, then it calls modify_bar
.
The solution is to ensure that the modify_bar
callback runs before the autosave.
One way to do that is with the prepend
option.
class Foo
belongs_to :bar, autosave: true
before_save :modify_bar, prepend: true
...
end
Another way would be to put the before_save
statement before the belongs_to
.
Another way would be to explicitly save bar
at the end of the modify_bar
method and not use the autosave
option at all.
Thanks to Danny Burkes for the helpful blog post.
** Also, they're run after all after_validation
callbacks and before any before_create
callbacks - see the docs.
Here's one way to check the order of such callbacks.
describe "sequence of callbacks" do
let(:sequence_checker) { SequenceChecker.new }
before :each do
foo.stub(:bar).and_return(sequence_checker)
end
it "modifies bar before saving it" do
# Run the before_save callbacks and halt before actually saving
foo.run_callbacks(:save) { false }
# Test one of the following
#
# If only these methods should have been called
expect(sequence_checker.called_methods).to eq(%w[modify save])
# If there may be other methods called in between
expect(sequence_checker.received_in_order?('modify', 'save')).to be_true
end
end
Using this supporting class:
class SequenceChecker
attr_accessor :called_methods
def initialize
self.called_methods = []
end
def method_missing(method_name, *args)
called_methods << method_name.to_s
end
def received_in_order?(*expected_methods)
expected_methods.map!(&:to_s)
called_methods & expected_methods == expected_methods
end
end
The above answer is (clearly) your solution. However:
I am fine using :autosave
, but I don't think changing external associations is a job for callbacks. I'm talking about your :modify_bar
. As brilliantly explained in this post I prefer to use another object to save multiple models at once. It really simplifies your life when things get more complex and makes tests much easier.
In this situation this might be accomplished in the controller or from a service object.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With