Nathan Long Nathan Long - 3 months ago 18
Ruby Question

Why isn't ActiveRecord's autosave working on my association?

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?

Answer

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 reverse the order of the belongs_to and the before_save statements.

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.


Update

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