Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 204
Nathan Long Avatar asked Feb 21 '13 21:02

Nathan Long


2 Answers

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.


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
like image 160
Nathan Long Avatar answered Nov 07 '22 21:11

Nathan Long


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.

like image 40
ecoologic Avatar answered Nov 07 '22 19:11

ecoologic