Search code examples
ruby-on-railspolymorphic-associations

Rails 5 update has_many through associates models


I have following models

class TeamPlayer < ApplicationRecord
  belongs_to :team
  belongs_to :player
  belongs_to :role
end

class Team < ApplicationRecord
  has_many :team_players
  has_many :players, :through => :team_players
end

class Role < ApplicationRecord
  has_many :team_players
  has_many :players, :through => :team_players
end

class Player < ApplicationRecord
  has_many :team_players
  has_many :teams, :through => :team_players

  has_many :roles, :through => :team_players
end

Basically, I want to assign different roles to different players in a team.

id  team_id     player_id   role_id 
2   1           2           1   
3   3           2           1   
4   1           1           2   

What should it look like in my teams_controller.rb to add new player with a role, to update a player with new role and to remove that player from my team?


Solution

  • This is only the start of a possible solution and it is pretty similar to what you have with some model and database validations added. Some of these validations ensure the uniqueness of every three-way relationship (FilledTeamRole), so either the error of attempting to create a duplicate record would need to be handled or you could filter the possible ids of each class that could be selected so that a duplicate cannot be created.

    A complete solution would depend on what other associations you want between the Team, Player and Role classes other than one that requires all three. For example, do you want/need an association between Team and Player where a relationship exists between only those two classes without the necessity of a Role (TeamPlayer id: 1, team_id: 1, player_id: 1). If those relationships are desired, then additional code will be needed to achieve this, which I have and can provide as a suggestion.

    As far as what your controller would look like, you could use the filled_team_roles controller (or perhaps create a dashboard controller), provide instance variables @teams, @players and @roles to populate drop-down menus for each class within a form to create the filled_team_roles relationship. You could also have additional forms within each of the other classes where, using two drop-downs instead of three with the third value the selected model id of the class whose controller the form is in (e.g. the edit action in the players_controller with drop-downs for team and role)

    ~/app/models/team.rb

    class Team < ApplicationRecord
      has_many :filled_team_roles, dependent: :destroy
      validates :name, uniqueness: { scope: [:sport, :city] }
      scope :by_name_asc, -> { order(name: :asc) }
    end
    

    ~/app/models/player.rb

    class Player < ApplicationRecord
      has_many :filled_team_roles, dependent: :destroy
      validates_uniqueness_of :ssn
      scope :by_name_asc, -> { order(last_name: :asc, first_name: :asc) }
    end
    

    ~/app/models/role.rb

    class Role < ApplicationRecord
      has_many :filled_team_roles, dependent: :destroy
      validates_uniqueness_of :name
      scope :by_name_asc, -> { order(name: :asc) }
    end
    

    ~/app/models/filled_team_role.rb

    class FilledTeamRole < ApplicationRecord
      belongs_to :team
      belongs_to :player
      belongs_to :role
      validates :team_id,   presence: true
      validates :player_id, presence: true
      validates :role_id,   presence: true
      validates :team_id, uniqueness: { scope: [:player_id, :role_id] }    
    end
    

    ~/db/migrate/20170127041000_create_team.rb

    class CreateTeam < ActiveRecord::Migration[5.0]
      def change
        create_table :teams do |t|
          t.string :name
          t.string :sport
          t.string :city
          t.string :state
          t.string :country
          t.timestamps null: false
        end
        add_index :teams, [:name, :sport, :city], unique: true
      end
    end
    

    ~/db/migrate/20170127041100_create_player.rb

    class CreatePlayer < ActiveRecord::Migration[5.0]
      def change
        create_table :players do |t|
          t.string :first_name
          t.string :last_name, index: true
          t.string :full_name_surname_first
          t.string :ssn, index: { unique: true }
          t.timestamps null: false
        end
      end
    end
    

    ~/db/migrate/20170127041200_create_role.rb

    class CreateRole < ActiveRecord::Migration[5.0]
      def change
        create_table :roles do |t|
          t.string :name, index: { unique: true }
          t.timestamps null: false
        end
      end
    end
    

    ~/db/migrate/20170127051300_create_filled_team_role.rb

    class CreateFilledTeamRole < ActiveRecord::Migration[5.0]
      def change
        create_table :filled_team_roles do |t|
          t.timestamps null: false
          t.references :team
          t.references :role
          t.references :player
        end
        add_index :filled_team_roles,
                 [:team_id, :player_id, :role_id],
                   unique: true,
                   name: 'index_filled_team_roles_unique_combination_of_foreign_keys'
      end
    end
    

    ~/db/seeds.rb

    Team.create(name: 'Los Angeles Dodgers', sport: 'baseball', city: 'Los Angeles', state: 'CA', country: 'United States')
    Team.create(name: 'New York Yankees',    sport: 'baseball', city: 'New York',    state: 'NY', country: 'United States')
    Team.create(name: 'Chicago Cubs',        sport: 'baseball', city: 'Chicago',     state: 'IL', country: 'United States')
    Team.create(name: 'St. Louis Cardinals', sport: 'baseball', city: 'St. Louis',   state: 'MO', country: 'United States')
    Player.create(first_name: 'Max',   last_name: 'Walker', full_name_surname_first: 'Walker, Max', ssn: '123-45-6789')
    Player.create(first_name: 'Homer', last_name: 'Winn',   full_name_surname_first: 'Winn, Homer', ssn: '234-56-7890')
    Player.create(first_name: 'Will',  last_name: 'Steel',  full_name_surname_first: 'Steel, Will', ssn: '345-67-8901')
    Player.create(first_name: 'Lucky', last_name: 'Snag',   full_name_surname_first: 'Snag, Lucky', ssn: '456-78-9012')
    Role.create(name: 'pitcher')
    Role.create(name: 'catcher')
    Role.create(name: 'first baseman')
    Role.create(name: 'second baseman')
    Role.create(name: 'shortstop')
    Role.create(name: 'third baseman')
    Role.create(name: 'right fielder')
    Role.create(name: 'center fielder')
    Role.create(name: 'left fielder')
    FilledTeamRole.create(team_id: 1, player_id: 1, role_id: 1)
    FilledTeamRole.create(team_id: 2, player_id: 2, role_id: 2)
    FilledTeamRole.create(team_id: 3, player_id: 3, role_id: 3)
    FilledTeamRole.create(team_id: 4, player_id: 4, role_id: 4)