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