Why isn't "before_remove" firing in this has_many association? - ruby-on-rails

I want to run some code before an object is removed from a has_many association.
I thought that I would be able to do this with the before_remove callback however for some reason this isn't firing and I don't understand why.
class Person < ActiveRecord::Base
has_many :limbs, before_remove: :print_message
def print_message
puts 'removing a limb'
end
end
class Limb < ActiveRecord::Base
belongs_to :person
end
While this code should print "removing a limb" during the destruction of a limb but it doesn't.
p = Person.create;
l = Limb.create person: p;
p.limbs.first.destroy
# SQL (2.1ms) DELETE FROM "limbs" WHERE "limbs"."id" = ? [["id", 5]]
# => #<Limb id: 5, person_id: 3, created_at: "2012-01-17 11:28:01", updated_at: "2012-01-17 11:28:01">
Why does this destroy action not cause the print_message method to run?
EDIT - does this before_remove callback exist?
A number of people have asked whether this callback exists. Although I can find very few further references to it, it is documented in the Rails documentation:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#label-Association+callbacks
It's an association callback though rather than a root ActiveRecord callback
Edit 2 - why not just use before_destroy on Limb?
Some people are asking why I'm not using the before_destroy callback on Limb. The reason is that I want person to check that there is a minimum number of limbs and that the last one is never destroyed. This is the original problem:
How do you ensure that has_many always "has a minimum"?

before_remove callback exists as an option in Associations callbacks. It's not the same as before_destroy, which is an ActiveRecord callback.
This is how you use it:
class Person < ActiveRecord::Base
has_many :limbs, :before_remove => :print_message
def print_message(limb)
# limb variable references to a Limb instance you're removing
# ( do some action here )
# ...
end
end
class Limb < ActiveRecord::Base
belongs_to :person
end
You're also calling a remove method incorrectly.
p = Person.create
l = Limb.create(:person => p)
p.limbs.first.destroy
Here you're calling it on Limb instance, that's why nothing is triggered.
Call it on an association you created:
p = Person.create
l = Limb.create(:person => p)
p.limbs.destroy(l)
EDIT
For preserving minimum of associated objects you can do something like this:
class Person < ActiveRecord::Base
has_many :limbs, :before_remove => :preserve_mimimum
def preserve_minimum(limb)
raise "Minimum must be preserved" if limbs.count == 1
end
end
class Limb < ActiveRecord::Base
belongs_to :person
end
This however does not get triggered on p.limbs.destroy_all, so you have to do something like this p.limbs.each {|l| p.limbs.destroy(l)}
Why it does not get triggered by destroy_all?
Because of this:
def destroy_all(conditions = nil)
find(:all, :conditions => conditions).each { |object| object.destroy }
end
It iterates over each element of an association and executes destroy action on an object and not on an association, that's why.

Replacebefore_remove with before_destroy.
Edit - handling minimum number of limbs
class Limb < ActiveRecord::Base
belongs_to :creature
before_destroy :count_limbs
def count_limbs
return false if self.creature.limbs.length <= self.creature.min_limbs
end
end
That return false will, I believe, stop it from being destroyed. Although I could be wrong

I can't say I've ever used the before_remove callback before, and not sure it exists.
The before destroy callback should be on the Limb model rather, and should look like this:
class Limb < ActiveRecord::Base
belongs_to :person
before_destroy :print_message
def print_message
puts 'removing a limb'
end
end

Related

Is there any before_update hook for an activerecord object if the object in the child relation is saved and touch is set?

I have
class Users < ActiveRecord::Base
belongs_to :team, touch: true
end
class Team < ActiveRecord::Base
has_many :users
before_save :update_popularity
before_update :update_popularity
def popularity
#secret algorythm
end
private
def update_popularity
# This is not called
end
end
User.first.name = 'John'
When I save a user I would like to update the team popularity as well. However the before_filter doesn't seem to be invoked?
Is there any proper way for doing this?
Try this before_update :update_popularity
Update:
After reviewing the API doc of touch method here, they say:
Please note that no validation is performed and only the after_touch callback is
executed.

How to mark has_one/has_many association as dirty?

I've been looking for this kind of functionality in AR, but don't seem able to find it. The Dirty implementation of AR states that an already persisted instance is deemed only dirty if one of its direct attributes has changed. So, let's say:
class Picture < ActiveRecord::Base
belongs_to :gallery
has_one :frame
end
in this case, I can do something like:
p = Picture.new(:gallery => Gallery.new, :frame => Frame.new)
p.save #=> it will save the three instances
p.gallery = Gallery.new
p.save #=> will not save the new gallery
p.gallery_id_will_change!
p.gallery = Gallery.new
p.save #=> this will save the new Gallery
but now I can't do something similar for the has_one association, since the Picture implementation doesn't own an attribute referring to it. So, it seems such dirty markups are impossible. Or aren't they?
Best thing I can figure out to do is:
class Picture < ActiveRecord::Base
belongs_to :gallery
has_one :frame
after_save :force_save_frame
def force_save_frame
frame.save! if frame.changed?
end
end
Like weexpectedTHIS said Dirty flags are set for attributes on the model itself, not for related objects. It only works for the belongs_to because there is a foreign key on the model.
But you can make a trick like that
module FrameDirtyStateTracking
def frame=(frame)
attribute_will_change!('frame')
super
end
end
class Picture < ActiveRecord::Base
prepend FrameDirtyStateTracking
belongs_to :gallery
has_one :frame
end
picture = Picture.new
picture.frame = Frame.new
picture.changed? # => true
picture.changed # => ['frame']

Detect changes on existing ActiveRecord association

I am writing an ActiveRecord extension that needs to know when an association is modified. I know that generally I can use the :after_add and :after_remove callbacks but what if the association was already declared?
You could simply overwrite the setter for the association. That would also give you more freedom to find out about the changes, e.g. have the assoc object before and after the change E.g.
class User < ActiveRecord::Base
has_many :articles
def articles= new_array
old_array = self.articles
super new_array
# here you also could compare both arrays to find out about what changed
# e.g. old_array - new_array would yield articles which have been removed
# or new_array - old_array would give you the articles added
end
end
This also works with mass-assignment.
As you say, you can use after_add and after_remove callbacks. Additionally set after_commit filter for association models and notify "parent" about change.
class User < ActiveRecord::Base
has_many :articles, :after_add => :read, :after_remove => :read
def read(article)
# ;-)
end
end
class Article < ActiveRecord::Base
belongs_to :user
after_commit { user.read(self) }
end

Virtual attribute not holding value when calling via join model?

Maybe rookie problem, maybe not, maybe i am lack of OOT but I still can't answer why I cant get the the value of instance variable #upgrade which was assigned to true.
class OrderTemplate < ActiveRecord::Base
belongs_to :user
attr_writer :upgrade #to hold upgrade process across actions
def upgrade
#upgrade || false
end
def from_time
p self.inspect
------------------------> they looks same
p self.upgrade
------------------------> is true as is supposed to be
p self.user.order_template.inspect
------------------------> they looks same
p self.user.order_template.upgrade
------------------------> is false but i am expecting true
self.user.has_time_bonus?
end
end
class User < ActiveRecord::Base
has_one :order_template
def has_time_bonus?
p self.order_template.upgrade
------------------------> is false but i am expecting true
end
end
Please smack me.
The short version is "activerecord doesn't have an identity map" (or at least, it's not enabled). If you do
an_order_template.user.order_template
then user.order_template causes the OrderTemplate to be loaded from the database a second time, so you have two distinct in memory objects representing the same database row. The second copy won't have any in memory only changes (including your instance variable).
You can probably work around this by doing
class OrderTemplate < ActiveRecord::Base
belongs_to :user, :inverse_of => :order_template
end
class User < ActiveRecord::Base
has_one :order_template, :inverse_of => :user
end
the :inverse_of option helps Active Record join the dots so that when you do
an_order_template.user.order_template
rails knows that two order templates are the same object.

Active Relation: Retrieving records through an association?

I have the following models:
class User < ActiveRecord::Base
has_many :survey_takings
end
class SurveyTaking < ActiveRecord::Base
belongs_to :survey
def self.surveys_taken # must return surveys, not survey_takings
where(:state => 'completed').map(&:survey)
end
def self.last_survey_taken
surveys_taken.maximum(:position) # that's Survey#position
end
end
The goal is to be able to call #user.survey_takings.last_survey_taken from a controller. (That's contrived, but go with it; the general goal is to be able to call class methods on #user.survey_takings that can use relations on the associated surveys.)
In its current form, this code won't work; surveys_taken collapses the ActiveRelation into an array when I call .map(&:survey). Is there some way to instead return a relation for all the joined surveys? I can't just do this:
def self.surveys_taken
Survey.join(:survey_takings).where("survey_takings.state = 'completed'")
end
because #user.survey_takings.surveys_taken would join all the completed survey_takings, not just the completed survey_takings for #user.
I guess what I want is the equivalent of
class User < ActiveRecord::Base
has_many :survey_takings
has_many :surveys_taken, :through => :survey_takings, :source => :surveys
end
but I can't access that surveys_taken association from SurveyTaking.last_survey_taken.
If I'm understanding correctly you want to find completed surveys by a certain user? If so you can do:
Survey.join(:survey_takings).where("survey_takings.state = 'completed'", :user => #user)
Also it looks like instead of:
def self.surveys_taken
where(:state => 'completed').map(&:survey)
end
You may want to use scopes:
scope :surveys_taken, where(:state => 'completed')
I think what I'm looking for is this:
class SurveyTaking < ActiveRecord::Base
def self.surveys_taken
Survey.joins(:survey_takings).where("survey_takings.state = 'completed'").merge(self.scoped)
end
end
This way, SurveyTaking.surveys_taken returns surveys taken by anyone, but #user.survey_takings.surveys_taken returns surveys taken by #user. The key is merge(self.scoped).
Waiting for further comments before I accept..

Resources