开发者

Rails: Updating an object from within a callback of an associated object

I have a “parent class” called Exam and that has many instances of a Score class. I want to modify an attribute on the Exam instance when one of the associated scores is saved. I stripped all the classes down to this very simple example, which looks stupid, but illustrates the problem in its most basic form. Here are the classes.

class Exam < ActiveRecord::Base
  has_many :scores

  def score_saved
    # self.name is now "Software Engineering"
    self.name = "#{name}!"
    # self.name is now "Software Engineering!"
  end
end

class Score < ActiveRecord::Base
  belongs_to :exam
  belongs_to :course

  before_save :trigger_score_saved

  def trigger_score_saved
    exam.score_saved unless exam.nil?
  end
end

Then I run the following test:

class ExamTest < ActiveSupport::TestCase
  test "create new exam" do
    exam = Exam.new(:name => "Software Engineering 1")
    score = exam.scores.build(:grade => 80, :course => courses(:one))
    exam.save

    # self.name is still "Software Engineering" here
    assert_equal "Software Engineering 1!", exam.name 
  end 
end

The comments in the code already illustrate the problem: the update of the name attribute of the exam object does not take place. Mind you, the trigger_开发者_开发问答score_saved proc is executed, but the newly set value is not the one that’s eventually saved to the database. If I define a before_save :trigger_score_saved callback on the exam object itself, the name attribute does get updated correctly. So it seems to have something to do with the fact that there’s a cascading save going on and that maybe the parent exam object on which the save started is different from the score.exam object that I’m trying to modify the value of.

Can anyone explain what’s going on here and how I can successfully update a parent object’s attribute from within the callback of a “child object”?

Notes:

  • I use Rails 3 and Ruby 1.9.2
  • I’ve tried update_attribute(:name => "#{name}!") instead of self.name = "#{name}!", but both have the same effect


I've done some more investigating and as it turns out, my problem is actually very simply solved by specifying the :inverse_of attribute on the relevant associations:

class Exam < ActiveRecord::Base
  has_many :scores, :inverse_of => :exam
  ...
end

class Score < ActiveRecord::Base
  belongs_to :exam, :inverse_of => :scores
  ...
end

This way exam.scores.first.exam is the exact same instance as exam, since the :inverse_of attribute tells Rails to use the same object instance in memory!

Additionally, I want the exam to be updated on any CRUD action on any score, so also when I delete a score from the exam.scores collection. This is where association callbacks like :after_remove come in handy.

So all with these tools up my belt, it seems that I can move forward even without an Identity Map (even though, I definitely do see the value in having one of those in Rails).


As you surmised, there are different instances of the Exam class in memory that reference the same DB row. You could call #reload to refresh, or wait for the identity work to make it in a released Rails version.

Some references to the identity map:

  • ActiveRecord::ObjectMap - Identity Map for AR
  • ActiveRecord Identity Map Ruby Summer of Code
  • DataMapper Why - Identity Map
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜