Using Rails to properly model with has_many :through and belongs_to
I'm trying to find the best way to define models for team / member / person, where a person can be a member of many teams and a team can have many members. Along with the "member" relationship is the position which the person fills for the team. A team should also have exactly one head coach and one assistant coach. A person could be a head/assistant coach for more than one team.
Below is my current (futile) attempt:
class Team < ActiveRecord::Base
has_many :members
has_many :people, :through => :members
belongs_to :head_coach :class => 'Person'
belongs_to :assistant_coach :class => 'Person'
end
class Person < ActiveRecord::Base
has_many :teams
has_many :teams, :through => :members
end
class Member < ActiveRecord::Base
belongs_to :team
belongs_to :person
# has a "position" which is a string
end
This approach is causing me two problems:
The Team's belongs_to :head_coach and :assistant_coach doesn't work. Maybe it should be a has_one, but then I'm not sure it makes sense to put the belongs_to in Person (I want a FK in Team to Person). The example below shows that how I have it set-up doesn't jive with ActiveRecord:
irb(main):006:0> t = Team.find(1) => #<Team id: 1, name: "Champs", created_at: "2011-07-18 01:50:56", updated_at: "2011-07-19 01:47:26", head_coach: nil> irb(main):007:0> t.head_coach => nil irb(main):008:0> t.head_coach = Person.find(1) => #<Person id: 1, name: "Chris", created_at: "2011-07-18 01:52:34", updated_at: "2011-07-18 01:52:34"> irb(main):009:0> t.save => true irb(main):010:0> t.head_coach => #<Person id: 1, name: "Chris", created_at: "2011-07-18 01:52:34", updated_at: "2011-07-18 01:52:34"> irb(main):011:0> Team.find(1).head_coach => nil
The has_many :through seems to work but I haven't found a good way to list the positions for each person within a team. This is my current attempt within a view:
<% @team.people.each do |person| %> <%开发者_JAVA百科= person.name +" "+ @team.members.find_by_person_id(person).position %>
Is there an overall better approach to representing these relationships?
Thanks for the help!
-Chris
I ended up going with the following approach using the explicit foreign_key which is working OK.
class Team < ActiveRecord::Base
has_many :members
has_many :people, :through => :members
belongs_to :head_coach, :class => 'Person', :foreign_key head_coach_id
belongs_to :assistant_coach, :class => 'Person', :foreign_key assistant_coach_id
end
class Person < ActiveRecord::Base
has_many :teams, :through => :members
end
class Member < ActiveRecord::Base
belongs_to :team
belongs_to :person
# has a "position" which is a string
end
The only downside to date is I haven't been able to get the foreign keys to load automatically in the controller. I need to add code such as the following in order to save the associations:
# POST /teams
def create
@team = Team.new
@team.head_coach = Person.find(params[:head_coach])
@team.assistant_coach = Person.find(params[:assistant_coach])
....
Could you have a boolean field for head_coach and assistant_coach in your members table and use a validate unique rule with a scope.
EG Add to members table
is_head_coach:boolean
is_assistant_coach:boolean
Now in your member model have
validates_uniqueness_of :is_head_coach, :scope => [:team_id, :person_id]
validates_uniqueness_of :is_assistant_coach, :scope => [:team_id, :person_id]
Then you could use some named scopes in your person model or team model to find the head_coach.
As for listing all the positions couldn't you use:
@team.members.each do |member|
"#{member.person.name} #{member.position}"
If you want to sort them in a specific way maybe you could add a field called sort_order:integer to the members table and sort by that.
Update:
I take your point that this solution above is not scalable. What about if you use the position field in your member model and create a scope aka 'named scope' for it like the following:
scope :head_coach, lambda { where('position = 'head coach') }
Then you could use something like:
team.members.head_coach
You could go even further with this scope
scope :get_position, lambda{|pos| where('position = ?', pos)}
and use
team.members.get_position('head_coach')
You could write a custom validate method to check that you end up with on head_coach per team, etc.
精彩评论