4

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!

Dan
  • 2,299
  • 2
  • 20
  • 41
  • 2
    You know the method find? Your know the method create? Well, Rails has a method find_or_create_by_attribute! So in your case you could use find_or_create_by_name. However, since you are using nested attributes... [Accepts nested attributes for with find or create](http://stackoverflow.com/questions/3579924/accepts-nested-attributes-for-with-find-or-create). So yeah, this is a duplicate question. – Ashitaka Mar 30 '12 at 01:39

3 Answers3

1

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.

Dan
  • 2,299
  • 2
  • 20
  • 41
0

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
BookOfGreg
  • 3,550
  • 2
  • 42
  • 56
0

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
Harish Shetty
  • 64,083
  • 21
  • 152
  • 198
  • i tried this out it doesn't seem to be effective, I still get duplicate artists created. – Dan Mar 30 '12 at 04:47
  • Debug by adding `p artist` before returning false. See if the id is set correctly. – Harish Shetty Mar 30 '12 at 04:57
  • I add p artist before returning false but I get nothing. Is that return supposed to display on screen or in a log somewhere? Also here is the server log when I submit the form http://pastebin.com/AfwxRack – Dan Mar 31 '12 at 20:23
  • You should see the data displayed in server console (not the log file..) – Harish Shetty Mar 31 '12 at 21:19
  • ahh I see here is the return `{"name"=>"Daft Punk", "id"=>#}` also I apologize for my noobness – Dan Mar 31 '12 at 22:53
  • There was a bug in my code. I was assigning an `Artist` object to `id` rather than a `id`. Try now and see. – Harish Shetty Apr 01 '12 at 00:48
  • cool thats gives me the proper return now `{"name"=>"Daft Punk", "id"=>28}` however it is still inserting a new artist everytime. It always returns the same artist with p artist tho. – Dan Apr 01 '12 at 01:17