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 ofself.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
精彩评论