Dan
Dan

Reputation: 2319

Rails has_many through avoiding duplication

I have a has_many through association setup between a song model and an artist model. My code looks something like this

SongArtistMap Model

class SongArtistMap < ActiveRecord::Base
 belongs_to :song
 belongs_to :artist
end

Artist Model

class Artist < ActiveRecord::Base
 has_many :song_artist_maps
 has_many :songs, :through => :song_artist_maps

 validates_presence_of :name
end

Song Model

class Song < ActiveRecord::Base
  has_many :song_artist_maps
  has_many :artists, :through => :song_artist_maps
  accepts_nested_attributes_for :artists
end

I have a form where a user submits a song and enters in the song title and the song artist.

So when a user submits a song and my Artists table doesn't already have the artist for the song I want it to create that artist and setup the map in SongArtistMap

If a user submits a song with an artist that is already in the Artists table I just want the SongArtistMap created but the artist not duplicated.

Currently everytime a user submits a song a new artist gets created in my artists table even if the same one already exists and a SongArtistMap is created for that duplicated artist.

Any idea on how to tackle this issue? I feel like rails probably has some easy little trick to fix this already built in. Thanks!

Upvotes: 4

Views: 1017

Answers (3)

Dan
Dan

Reputation: 2319

Ok I got this figured out awhile ago and forgot to post. So here's how I fixed my problem. First of all I realized I didn't need to have a has_many through relationship.

What I really needed was a has_and_belongs_to_many relationship. I setup that up and made the table for it.

Then in my Artists model I added this

def self.find_or_create_by_name(name)
  k = self.find_by_name(name)

  if k.nil?
    k = self.new(:name => name)
  end

  return k
end

And in my Song model I added this

before_save :get_artists
def get_artists
  self.artists.map! do |artist|
   Artist.find_or_create_by_name(artist.name)
  end
end

And that did exactly what I wanted.

Upvotes: 1

Harish Shetty
Harish Shetty

Reputation: 64363

Try this:

class Song < ActiveRecord::Base
  has_many :song_artist_maps
  has_many :artists, :through => :song_artist_maps
  accepts_nested_attributes_for :artists, :reject_if => :normalize_artist


  def normalize_artist(artist)
    return true if  artist['name'].blank?
    artist['id'] = Artist.find_or_create_by_name(artist['name']).id
    false # This is needed
  end
end

We are essentially tricking rails by over-loading the reject_if function(as we never return true).

You can further optimize this by doing case insensitive lookup ( not required if you are on MySQL)

    artist['id'] = ( 
     Artist.where("LOWER(name) = ? ", artist['name'].downcase).first ||       
     Artist.create(:name => artist['name'])
    ).id

Upvotes: 0

BookOfGreg
BookOfGreg

Reputation: 3706

I use a method in the model of the table the other two go through, that is called with before_create. This can probably be made much neater and faster though.

before_create :ensure_only_one_instance_of_a_user_in_a_group

  private

  def ensure_only_one_instance_of_a_user_in_a_group
    user = User.find_by_id(self.user_id)
    unless user.groups.empty?
      user.groups.each do |g|
        if g.id == self.group_id
          return false
        end
      end
    end
    return true
  end

Upvotes: 0

Related Questions