Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple Associations to Same Model

I have two classes that I would like to specify as follows:

class Club < ActiveRecord::Base
  belongs_to :president, :class_name => "Person", :foreign_key => "president_id"
  belongs_to :vice_president, 
             :class_name => "Person",
             :foreign_key => "vice_president_id"
end

class Person < ActiveRecord::Base
  has_one :club, :conditions => 
  ['president_id = ? OR vice_president_id = ?', '#{self.id}', '#{self.id}']
end

This doesn't work and gives me an error when trying to get the club association from the person object. The error is because is looking for person_id in the club table when I looked at the SQL. I can get around it by declaring multiple has_one associations, but feel like this is the improper way of doing it.

A person can only be the President or Vice President of one club.

Anyone able to offer a little bit of advice on this issue, I would be very appreciative.

like image 883
adimitri Avatar asked Feb 08 '10 04:02

adimitri


1 Answers

Your has_one condition will never work in Rails, as far as I know.

You need one explicit has_one or belongs_to or has_many per "link", on both tables. So if you have two "links", you need two has_one and two belongs_to. That is how it works.

Secondly, I think you should reconsider your models. The way you are doing it, one person can not be the president of a club and an employee, at the same time. Or be the president of two clubs. Even if you don't have these right now, they can come in the future - it is easier to stay flexible right now.

A flexible way of doing this is using a has_many :through with an intermediate table that specifies the role. In other words:

# The memberships table has a person_id, club_id and role_id, all integers

class Membership < ActiveRecord::Base
  belongs_to :club
  belongs_to :person
  validates_presence_of :role_id
  validates_numericality_of :role_id
end

class Club < ActiveRecord::Base
  has_many :memberships, :dependent => :delete_all
  has_many :people, :through => :memberships
end

class Person < ActiveRecord::Base
  has_many :memberships, :dependent => :delete_all
  has_many :clubs, :through => :memberships
end

Now, assuming that role_id=0 means employee, role_id=1 means president, and role_id=2 means vice_president, you can use it like this:

tyler = Person.find(1) # person_id is 1
other = Person.find(2) # person_id is 2
c = Club.find(1)  # club_id is 1

tyler.clubs # returns all the clubs this person is "member" of
c.people # returns all the "members" of this club, no matter their role

#make tyler the president of c
tyler.memberships.create(:club_id => 1, :role_id => 1)

#make other the vicepresident of c
#but using c.memberships instead of other.memberships (works both ways)
c.memberships.create(:person_id => 2, :role_id => 1)

#find the (first) president of c
c.memberships.find_by_role_id(1).person

#find the (first) vicepresident of c
c.memberships.find_by_role_id(2).person

#find all the employees of c
c.memberships.find_all_by_role_id(0).collect { |m| m.person }

#find all the clubs of which tyler is president
tyler.memberships.find_all_by_role_id(1).collect { |m| m.club }

Additional notes:

  • You could complement this with a roles table and model. Roles would have just a a name, roles would have_many relationships and memberships would belong_to role. Or, you could define methods in memberships for getting the role name (if 0, it returns "employee", if 1, "president", etc
  • You can add validations on memberhips so no more than 1 person can be made president of a given club, or the same employee on the same club twice. Later on, if you start getting "exceptional cases" in which a person needs to be in two places, you will just have to adapt your validations.
like image 199
kikito Avatar answered Nov 20 '22 20:11

kikito