简体   繁体   中英

incorrect database records created for rails 3 has_many :through association

I have a has_many :through association. Players have many Teams and Teams have many Players. The join model, Affiliation, belongs to Players and Teams, and also has a year attribute to keep track of a player's team affiliation (or employment) from year to year.

I can't seem to figure out the right way to build an association based on the following rules:

  1. Create a new player.
  2. Associate a team that may be new or existing. So find it or create it, but only create it if the player is saved.
  3. The association may or may not include a year, but the association should only be created if the player and team are saved.

The Player model looks like:

class Player < ActiveRecord::Base
  attr_accessible :name

  has_many :affiliations, :dependent => :destroy
  has_many :teams, :through => :affiliations
end

The Team model looks like:

class Team < ActiveRecord::Base
  attr_accessible :city

  has_many :affiliations, :dependent => :destroy
  has_many :players, :through => :affiliations
end

The Affiliation model looks like:

class Affiliation < ActiveRecord::Base
  attr_accessible :player_id, :team_id, :year
  belongs_to :player
  belongs_to :team
end

I have been successful at creating the association records without the join model attribute using a create action in the PlayersController that looks like:

class PlayersController < ApplicationController
  def create
    @player = Player.new(params[:player].except(:teams))

    unless params[:player][:teams].blank?
      params[:player][:teams].each do |team|
        team_to_associate = Team.find_or_initialize_by_id(team[:id], team.except(:year)
        @player.teams << team_to_associate
      end
    end

    @player.save
    respond_with @player
  end
end

After creating a new player with two teams using params like:

{"player"=>{"name"=>"George Baker", "teams"=>[{"city"=>"Buffalo"}, {"city"=>"Detroit"}]}}

the database looks like:

players

id: 1, name: George Baker

teams

id: 1, city: Buffalo

id: 2, city: Seattle

affiliations

id: 1, player_id: 1, team_id: 1, year: null

id: 2, player_id: 1, team_id: 2, year: null

When I try to introduce the year, things fall apart. My most recent attempt at the create action in the PlayersController looks like:

class PlayersController < ApplicationController
  def create
    @player = Player.new(params[:player].except(:teams))

    unless params[:player][:teams].blank?
      params[:player][:teams].each do |team|
        team_to_associate = Team.find_or_initialize_by_id(team[:id], team.except(:year)
        // only additional line...
        team_to_associate.affiliations.build({:year => team[:year]})
        @player.teams << team_to_associate
      end
    end

    @player.save
    respond_with @player
  end
end

Now, when creating a new player with two teams using params like:

{"player"=>{"name"=>"Bill Johnson", "teams"=>[{"id"=>"1"}, {"city"=>"Detroit", "year"=>"1999"}]}}

the database looks like:

players

id: 1, name: George Baker

id: 2, name: Bill Johnson

teams

id: 1, city: Buffalo

id: 2, city: Seattle

id: 3, city: Detroit

affiliations

id: 1, player_id: 1, team_id: 1, year: null

id: 2, player_id: 1, team_id: 2, year: null

id: 3, player_id: 2, team_id: 1, year: null

id: 4, player_id: null, team_id: 3, year: 1999

id: 5, player_id: 2, team_id: 3, year: null

So three records were created when only two should have been. The affiliation record id: 3 is correct. For id: 4, the player_id is missing. And for id: 5, the year is missing.

Obviously this is incorrect. Where am I going wrong?

Thanks

Edit

Ok, i think i have a better solution. AFAIK, you can't use nested attributes on two levels of depth (though you could test it, maybe it works), but nothing prevents us to simulate this behavior :

class Player < ActiveRecord::Base
  has_many :affiliations
  has_many :teams, through: :affiliations
  accespts_nested_attributes_for :affiliations, allow_destroy: true
end

class Affiliation < ActiveRecord::Base
  belongs_to :player
  belongs_to :team

  validates :player, presence: true
  validates :team,   presence: true

  attr_accessor :team_attributes 

  before_validation :link_team_for_nested_assignment

  def link_team_for_nested_assignment
    return true unless team.blank?
    self.team = Team.find_or_create_by_id( team_attributes )
  end

Now, doing this :

@player = Player.new( 
            name: 'Bill Johnson', 
            affiliations_attributes: [
              {year: 1999, team_attributes: {id: 1, city: 'Detroit}},
              {team_attributes: {city: 'Somewhere else'}}
            ]
          )
@player.save

should create all the required records, and still rollback everything in case of problems (because the save itself is already wrapped in a transaction). As a bonus, all the errors will be associated to @player !

How about this ?

 
 
 
  
  class PlayersController < ApplicationController def create ActiveRecord::Base.transaction do @player = Player.new(params[:player].except(:teams)) raise ActiveRecord::Rollback unless @player.save # first check unless params[:player][:teams].blank? @teams = [] params[:player][:teams].each do |team| team_to_associate = Team.find_or_initialize_by_id(team[:id], team.except(:year)) raise ActiveRecord::Rollback unless team_to_associate.save # second check if team[:year] affiliation = team_to_associate.affiliations.build(player: @player, year: team[:year]) raise ActiveRecord::Rollback unless affiliation.save # third check end @teams << team_to_associate # keep the object so we have access to errors end end end flash[:notice] = "ok" rescue ActiveRecord::Rollback => e flash[:alert] = "nope" ensure respond_with @group end end
 
  

This solution ended up working for me. If anyone uses this code for their own project, however, please know that I haven't tested any other actions besides create. I'm certain some of this will change once I deal with read, update and delete.

class Player < ActiveRecord::Base
  attr_accessible :name

  has_many :affiliations, :dependent => :destroy
  has_many :teams, :through => :affiliations
  accepts_nested_attributes_for :affiliations, :allow_destroy => true
  attr_accessible :affiliations_attributes
end

class Team < ActiveRecord::Base
  attr_accessible :city

  has_many :affiliations, :dependent => :destroy
  has_many :players, :through => :affiliations
end

class Affiliation < ActiveRecord::Base
  attr_accessible :player_id, :team_id, :team_attributes, :year
  belongs_to :player
  belongs_to :team
  accepts_nested_attributes_for :team

  def team_attributes=(team_attributes)
    self.team = Team.find_by_id(team_attributes[:id])
    self.team = Team.new(team_attributes.except(:id)) if self.team.blank?
  end
end

class PlayersController < ApplicationController
  def create
    player_params = params[:player].except(:teams)
    affiliation_params = []

    unless params[:player][:teams].blank?
      params[:player][:teams].each do |team|
        affiliation = {}
        affiliation[:year] = team[:year] unless team[:year].blank?
        affiliation[:team_attributes] = team.except(:year)
        affiliation_params << affiliation
      end
    end

    player_params[:affiliation_attributes] = affiliation_params unless affiliation_params.blank?

    @player = Player.new(player_params)
    @player.save

    respond_with @player
  end
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM