4

I am trying to associate existing records while still being able to add new ones. The following does not work but is pretty close to what I need. How can I accomplish associating existing records and creating new ones?

has_many :comments, :through => :commentings, :source => :commentable, :source_type => "Comment"
accepts_nested_attributes_for :comments, :allow_destroy => true

def autosave_associated_records_for_comments
  comments.each do |comment|
    if existing_comment = Comment.find_by_fax_and_name(comment.fax, comment.name)
      self.comments.reject! { |hl| hl.fax == existing_comment.fax && hl.name == existing_comment.name }
      self.comments << existing_comment
    else
      self.comments << comment
    end
  end
end

Here is a relevant line of source: https://github.com/rails/rails/blob/v3.0.11/activerecord/lib/active_record/autosave_association.rb#L155

maletor
  • 7,072
  • 7
  • 42
  • 63
  • Thanks for this, +1 for the source reference. Fits the bill perfectly for something I'm working on! – stuartc Sep 14 '12 at 09:27

3 Answers3

6

I've made a solution, but if you know of a better way to do this please let me know!

def autosave_associated_records_for_comments
  existing_comments = []
  new_comments = []

  comments.each do |comment|
    if existing_comment = Comment.find_by_fax_and_name(comment.fax, comment.name)
      existing_comments << existing_comment
    else
      new_comments << comment
    end
  end

  self.comments << new_comments + existing_comments
end
maletor
  • 7,072
  • 7
  • 42
  • 63
  • Hi @maletor, It's worked fine but problem occur while updating.. Duplicate record gets inserted. so i followed this and its worked for me. http://stackoverflow.com/questions/3579924/accepts-nested-attributes-for-with-find-or-create – Gagan Jan 24 '14 at 06:48
1

I have a tagging system that utilizes a has_many :through relationship. Neither of the solutions here got me where I needed to go so I came up with a solution that may help others. This has been tested on Rails 3.2.

Setup

Here are a basic version of my Models:

Location Object:

class Location < ActiveRecord::Base
    has_many :city_taggables, :as => :city_taggable, :dependent => :destroy
    has_many :city_tags, :through => :city_taggables

    accepts_nested_attributes_for :city_tags, :reject_if => :all_blank, allow_destroy: true
end

Tag Objects

class CityTaggable < ActiveRecord::Base
   belongs_to :city_tag
   belongs_to :city_taggable, :polymorphic => true
end

class CityTag < ActiveRecord::Base
   has_many :city_taggables, :dependent => :destroy
   has_many :ads, :through => :city_taggables
end

Solution

I did indeed override the autosave_associated_record_for method as follows:

class Location < ActiveRecord::Base
   private

   def autosave_associated_records_for_city_tags
     tags =[]
     #For Each Tag
     city_tags.each do |tag|
       #Destroy Tag if set to _destroy
       if tag._destroy
         #remove tag from object don't destroy the tag
         self.city_tags.delete(tag)
         next
       end

       #Check if the tag we are saving is new (no ID passed)
       if tag.new_record?
         #Find existing tag or use new tag if not found
         tag = CityTag.find_by_label(tag.label) || StateTag.create(label: tag.label)
       else
         #If tag being saved has an ID then it exists we want to see if the label has changed
         #We find the record and compare explicitly, this saves us when we are removing tags.
         existing = CityTag.find_by_id(tag.id)
         if existing    
           #Tag labels are different so we want to find or create a new tag (rather than updating the exiting tag label)
           if tag.label != existing.label
             self.city_tags.delete(tag)
             tag = CityTag.find_by_label(tag.label) || CityTag.create(label: tag.label)
           end
         else
           #Looks like we are removing the tag and need to delete it from this object
           self.city_tags.delete(tag)
           next
         end
       end
       tags << tag
     end
     #Iterate through tags and add to my Location unless they are already associated.
     tags.each do |tag|
       unless tag.in? self.city_tags
         self.city_tags << tag
       end
     end
   end

The above implementation saves, deletes and changes tags the way I needed when using fields_for in a nested form. I'm open to feedback if there are ways to simplify. It is important to point out that I am explicitly changing tags when the label changes rather than updating the tag label.

Dustin M.
  • 2,884
  • 3
  • 20
  • 18
1

In Rails 6 (and probably earlier versions too), it's easy to overwrite the attributes writer generated by accepts_nested_attributes_for and use find_or_initialize_by.

In this case, you could simply write:

has_many :comments, :through => :commentings, :source => :commentable, :source_type => "Comment"
accepts_nested_attributes_for :comments, :allow_destroy => true

def comments_attributes=(hashes)
  hashes.each { |attributes| comments << Comment.find_or_initialize_by(attributes)
end

Haven't tested this when passing :id or :_destroy keys, as it doesn't apply in my situation, but feel free to share your thoughts or code if you do.

I would love to see Rails implement this natively, perhaps by passing an upsert: true option to accepts_nested_attributes_for.

Goulven
  • 777
  • 9
  • 20