views:

32

answers:

1

Hi, I'd like to create a self referencing relation in rails. I have a Person model, and the person should have masters and pupils with same Person object.

So far I tried:

class Person <ActiveRecord::Base
   has_many :relationships, :dependent => :destroy
   has_many :masters, :through => :relationships, :conditions => "status='master'"
   has_many :pupils, :through => :relationships, :conditions => "status='pupil'"
   has_many :inverse_relationships, :class_name => "Relationship",
      :foreign_key => "related_id"
   has_many :inverse_masters, :through => :inverse_relationships,  
      :source => :person, :conditions => "status='master'"
   has_many :inverse_pupils, :through => :inverse_relationships,  
      :source => :person, :conditions => "status='pupil'"
end

class Relationship < ActiveRecord::Base
  belongs_to :person
  belongs_to :master, :class_name => "Person", :foreign_key => 'related_id'
  belongs_to :pupil, :class_name => "Person", :foreign_key => 'related_id'
end

It seems to work when I am trying to select:

@a = Person.find(:first)
@a.masters

but when I try to do a push into masters, it saves the relationship without the status set to master. It saves null instead. Is there an easy way to save status=master when I push into masters and status=pupil when I push into pupils?

Thanks

+1  A: 

To make it short the solution is: association callbacks (more here under the Association Callback section: http://railsapi.com/doc/rails-v3.0.0/classes/ActiveRecord/Associations/ClassMethods.html)

To be a little more detailed I have adapted your example a little bit, but basically the structure is the same, here is the code:

class Person < ActiveRecord::Base
  has_many :relationships
  has_many :pupils, :through => :relationships, :source => :other_person, :conditions => 'relationships.type = "MasterPupil"', :after_add => Proc.new{|p,o| Relationship.update_all("type = 'MasterPupil'", ['person_id = ? AND other_person_id = ?', p.id, o.id])}
  has_many :masters, :through => :relationships, :source => :other_person, :conditions => 'relationships.type = "PupilMaster"', :after_add => Proc.new{|p,o| Relationship.update_all("type = 'PupilMaster'", ['person_id = ? AND other_person_id = ?', p.id, o.id])}
end

class Relationship < ActiveRecord::Base
  belongs_to :person
  belongs_to :other_person, :class_name => 'Person'
  before_validation :set_type

  def set_type
    self.type = 'OpenRelationship'
  end
end

class MasterPupil < Relationship
end

class PupilMaster < Relationship
end

The RelationShip model contains a type column which is the equivalent of your status column, but type is nicer if I later want to do an STI and declare MasterPupil/PupilMaster relationship models.

RelationShip also has a set_type before_validation that will set the type to OpenRelationship which should be temporary before the after_add callback defined in the Person model in each association will set things clear (and set either a MasterPupil or PupilMaster type)

and now:

Loading development environment (Rails 3.0.0)
irb(main):001:0> p = Person.create
=> #<Person id: 1, created_at: "2010-09-10 23:35:37", updated_at: "2010-09-10 23:35:37">
irb(main):002:0> p.pupils
=> []
irb(main):003:0> p.masters
=> []
irb(main):004:0> p.pupils << Person.create
=> [#<Person id: 2, created_at: "2010-09-10 23:35:56", updated_at: "2010-09-10 23:35:56">]
irb(main):005:0> Relationship.all
=> [#<MasterPupil id: 1, person_id: 1, other_person_id: 2, type: "MasterPupil">]
irb(main):006:0> p.masters << Person.create
=> [#<Person id: 3, created_at: "2010-09-10 23:36:29", updated_at: "2010-09-10 23:36:29">]
irb(main):007:0> Relationship.all
=> [#<MasterPupil id: 1, person_id: 1, other_person_id: 2, type: "MasterPupil">, #<PupilMaster id: 2, person_id: 1, other_person_id: 3, type: "PupilMaster">]
irb(main):008:0> p.reload
=> #<Person id: 1, created_at: "2010-09-10 23:35:37", updated_at: "2010-09-10 23:35:37">
irb(main):009:0> p.pupils
=> [#<Person id: 2, created_at: "2010-09-10 23:35:56", updated_at: "2010-09-10 23:35:56">]
irb(main):010:0> p.masters
=> [#<Person id: 3, created_at: "2010-09-10 23:36:29", updated_at: "2010-09-10 23:36:29">]
hellvinz
That's what I thought, I'll need rails 3 for it. How backward compatible is r3? Though I probably can't loose anything with a good backup;) I tried this solution but I have 2.3.8 and it doesn't have the association callbacks yet. I checked the valid_keywords in the source for it;) Thanks man, I'll get back to this tomorrow, but I guess it is perfect, as I tried it, only the version didn't match.
Tamisoft
OK, it seems it works perfectly on 2.3.8 as well, though some said on forums that it might have bugs, but for now I am all set. Thank you very much for the help
Tamisoft
no problem. just vote for my answer and it will be great :)
hellvinz
One more question about it, when I have this person model to be used as the Author as well, and I want to manage the masters pupils, can I do some sort of super call to call everything that the person has? I have a Class Author << Person doing the assoc, but when I post on a person who is an author, the form posts to the author edit link.
Tamisoft