Home > Back-end >  How can you see what associated model triggered a callback in Rails?
How can you see what associated model triggered a callback in Rails?

Time:05-21

Suppose that Student belongs_to :contact and Teacher also belongs_to :contact. Suppose that first_name is in Contact, and Student and Teacher have a first name through the Contact and has accepts_nested_attributes_for :contact.

In an after_commit callback, if first_name changes, I want to see if it came from Student or from Teacher. I'm not sure how to do this though, or whether it's even possible. Inside of Student's after_commit, previous_changes doesn't have first_name. It's only in Contact's after_commit that it shows up. So inside of Contact's after_commit I want to see if it originated from Student or from Teacher.

How can I do that? Is there something similar to destroyed_by_association? When I search for "by_association" in the docs I don't see anything else, nor do I see anything else in Active Record Autosave Association.

Note: It is possible for a Contact to have both a Student and a Teacher, so it isn't as simple as just saying that if the Contact doesn't have a Student it must have came from Teacher.

CodePudding user response:

If you add corresponding associations to Contact they will be accessible in after_commit callback:

class Student < ApplicationRecord
  belongs_to :contact
  accepts_nested_attributes_for :contact
end

class Teacher < ApplicationRecord
  belongs_to :contact
  accepts_nested_attributes_for :contact
end

class Contact < ApplicationRecord
  has_one :teacher
  has_one :student

  after_commit do
    p teacher || student
  end
end
>> Teacher.create(contact_attributes: { name: 'teacher' })
...
# after_commit START
#<Teacher id: 1, contact_id: 1>
# after_commit END
=> #<Teacher:0x00007fd28056a9f0 id: 1, contact_id: 1>

One caveat is that if teacher is not present it will hit the database:

>> Student.create(contact_attributes: { name: 'student' });
 ...
# after_commit START
  Teacher Load (0.4ms)  SELECT "teachers".* FROM "teachers" WHERE "teachers"."contact_id" = $1 LIMIT $2  [["contact_id", 2], ["LIMIT", 1]]
#<Student id: 1, contact_id: 2>
# after_commit END
=> #<Student:0x00007fd2837bd9c0 id: 1, contact_id: 2>

Switching to polymorphic relationship is also a solution:

class Student < ApplicationRecord
  has_one :contact, as: :contactable
  accepts_nested_attributes_for :contact
end

class Teacher < ApplicationRecord
  has_one :contact, as: :contactable
  accepts_nested_attributes_for :contact
end

# NOTE: to add plymorphic relationship add this to contact migration:
#       t.references :contactable, polymorphic: true
class Contact < ApplicationRecord
  belongs_to :contactable, polymorphic: true

  after_commit do
    p contactable
  end
end
>> Student.create(contact_attributes: { name: 'student' })
 ...
# after_commit START        
#<Student id: 1>            
# after_commit END          
=> #<Student:0x00007f4a85c0d1c8 id: 1>

>> Teacher.create(contact_attributes: { name: 'teacher' })
 ...
# after_commit START        
#<Teacher id: 1>            
# after_commit END          
=> #<Teacher:0x00007f4a884e2a08 id: 1>

Update

You have access to everything if you put the callback in Student and Teacher, regardless of the above setup.

Polymorphic:

class Teacher < ApplicationRecord
  has_one :contact, as: :contactable
  accepts_nested_attributes_for :contact

  after_commit do
    p self
    p contact
    p contact.previous_changes
  end
end
>> Teacher.create(contact_attributes: { name: 'teacher' })
 ...
# after_commit START
#<Teacher id: 1>
#<Contact id: 1, name: "teacher", contactable_type: "Teacher", contactable_id: 1>
{"id"=>[nil, 1], "name"=>[nil, "teacher"], "contactable_type"=>[nil, "Teacher"], "contactable_id"=>[nil, 1]}
# after_commit END
=> #<Teacher:0x00007fbf1bdd2db8 id: 1>

Belongs_to:

class Teacher < ApplicationRecord
  belongs_to :contact
  accepts_nested_attributes_for :contact

  after_commit do
    p self
    p contact
    p contact.previous_changes
  end
end
>> Teacher.create(contact_attributes: { name: 'teacher' })
   ...
# after_commit START        
#<Teacher id: 1, contact_id: 1>
#<Contact id: 1, name: "teacher">
{"id"=>[nil, 1], "name"=>[nil, "teacher"]}
# after_commit END          
=> #<Teacher:0x00007f9f2ce6a730 id: 1, contact_id: 1>

If you must have after_commit in Contact you have to explicitly tell Contact that update came from Teacher or Student:

class Teacher < ApplicationRecord
  belongs_to :contact
  accepts_nested_attributes_for :contact

  # NOTE: before saving, let contact know it's about to be updated
  #       by Teacher.
  before_save do
    contact.updated_from = :teacher
  end
end

class Contact < ApplicationRecord
  # NOTE: just a temporary attribute
  attr_accessor :updated_from

  after_commit do
    print 'Updated from: '
    p self.updated_from
  end
end
>> Teacher.create(contact_attributes: { name: 'teacher' })
 ...
# after_commit START
Updated from: :teacher      
# after_commit END
=> #<Teacher:0x00007fd6a40459c0 id: 1, contact_id: 1>

Or without before_save callback in Teacher:

>> Teacher.create(contact_attributes: { name: 'teacher', updated_from: 'teacher' })
 ...
Updated from: "teacher"  
  • Related