Accepts_nested_attributes_for with find_or_create?

I use the accepts_nested_attributes_for Rails method with great success, but how can I make it not create new records if the record already exists?

As an example:

Let's say I have three models: Team, Membership, and Player, and each team has players from several players through membership, and players can belong to many teams. The Team model can then accept nested attributes for the players, but this means that each player represented in the form of a combined team + player (s) will be created as a new player record.

How do I do something if I only want to create a new player record this way, if there is no player with the same name yet? If there is a player with the same name, new player records should not be created, but instead the correct player should be found and linked to the new team record.

+51
validation ruby-on-rails activerecord forms
Aug 26 '10 at
source share
8 answers

When you define a hook for autosave associations, the normal code path is skipped and the method is called instead. So you can do this:

class Post < ActiveRecord::Base belongs_to :author, :autosave => true accepts_nested_attributes_for :author # If you need to validate the associated record, you can add a method like this: # validate_associated_record_for_author def autosave_associated_records_for_author # Find or create the author by name if new_author = Author.find_by_name(author.name) self.author = new_author else self.author.save! end end end 

This code has not been verified, but it should be pretty much what you need.

+45
Aug 26 '10 at 23:14
source share

Do not think about it, adding players to teams, think about it, adding membership to teams. The form does not work directly with players. The Membership model can have a virtual attribute player_name . Behind the scenes, this can either find a player or create one.

 class Membership < ActiveRecord::Base def player_name player && player.name end def player_name=(name) self.player = Player.find_or_create_by_name(name) unless name.blank? end end 

And then just add the player_name text box to any membership form constructor.

 <%= f.text_field :player_name %> 

Thus, it is not specific to accepts_nested_attributes_for and can be used in any form of membership.

Note. With this method, a player model is created before verification. If you do not want this effect, save the player in the instance variable and then save it in the before_save callback.

+27
May 14 '11 at 15:08
source share

When using :accepts_nested_attributes_for sending the id existing record will force ActiveRecord to update the existing record instead of creating a new record. I'm not sure what your markup is, but try something like this:

 <%= text_field_tag "team[player][name]", current_player.name %> <%= hidden_field_tag "team[player][id]", current_player.id if current_player %> 

Player name will be updated if id provided, but created differently.

The approach to defining the autosave_associated_record_for_ method autosave_associated_record_for_ very interesting. I will definitely use it! However, consider this simpler solution.

+4
Feb 17 2018-12-17T00:
source share

To get around the question in terms of the question (referenced by find_or_create), the if block in Francois's answer can be rephrased as:

 self.author = Author.find_or_create_by_name(author.name) unless author.name.blank? self.author.save! 
+3
Feb 25 2018-11-11T00:
source share

This works great if you have a has_one or belongs_to relationship. But lacked has_many or has_many through.

I have a tag system that uses has_many: through relationships. None of the solutions here led me to where I needed to go, so I came up with a solution that could help others. This has been tested on Rails 3.2.

Customization

Here is the 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 

Decision

I really redefined the autosave_associated_recored_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) || CityTag.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 modifies tags as I needed, using the fields for the nested form. I am open to feedback if there are ways to simplify. It is important to note that I explicitly change tags when the label changes, rather than updating the tag label.

+3
Sep 24 '14 at 18:02
source share

A before_validation hook is a good choice: a standard mechanism that provides simpler code than overriding the more obscure autosave_associated_records_for_* .

 class Quux < ActiveRecord::Base has_and_belongs_to_many :foos accepts_nested_attributes_for :foos, reject_if: ->(object){ object[:value].blank? } before_validation :find_foos def find_foos self.foos = self.foos.map do |object| Foo.where(value: object.value).first_or_initialize end end end 
+1
Nov 20 '15 at 12:01
source share

@ dustin-m the answer was useful to me - I am doing something custom with has_many: through relationships. I have a Topic in which there is one Trend that has many children (recursive).

ActiveRecord does not like it when I configure it as the standard relation has_many :searches, through: trend, source: :children . It retrieves topic.trend and topic.searches, but will not do topic.searches.create (name: foo).

So, I used the above to create custom autosave and achieved the correct result with accepts_nested_attributes_for :searches, allow_destroy: true def autosave_associated_records_for_searches searches.each do | s | if s._destroy self.trend.children.delete(s) elsif s.new_record? self.trend.children << s else s.save end end end def autosave_associated_records_for_searches searches.each do | s | if s._destroy self.trend.children.delete(s) elsif s.new_record? self.trend.children << s else s.save end end end

0
Dec 28 '16 at 21:54
source share

Answer by @ FranΓ§ois Beausoleil is awesome and solve a large problem. It's great to learn about the concept of autosave_associated_record_for .

However, in this implementation, I found one edge example. In case of update existing author of the message ( A1 ), if the name of the new author ( A2 ) is transmitted, it will eventually change the original name of the author ( A1 ).

 p = Post.first p.author #<Author id: 1, name: 'JK Rowling'> # now edit is triggered, and new author(non existing) is passed(eg: Cal Newport). p.author #<Author id: 1, name: 'Cal Newport'> 

Indicative code:

 class Post < ActiveRecord::Base belongs_to :author, :autosave => true accepts_nested_attributes_for :author # If you need to validate the associated record, you can add a method like this: # validate_associated_record_for_author def autosave_associated_records_for_author # Find or create the author by name if new_author = Author.find_by_name(author.name) self.author = new_author else self.author.save! end end end 

This is because in the case of editing self.author for the message will already be an author with id: 1, it will go into another, block and update this author instead of creating a new one.

I changed the code ( elsif condition) to mitigate this problem:

 class Post < ActiveRecord::Base belongs_to :author, :autosave => true accepts_nested_attributes_for :author # If you need to validate the associated record, you can add a method like this: # validate_associated_record_for_author def autosave_associated_records_for_author # Find or create the author by name if new_author = Author.find_by_name(author.name) self.author = new_author elsif author && author.persisted? && author.changed? # New condition: if author is already allocated to post, but is changed, create a new author. self.author = Author.new(name: author.name) else # else create a new author self.author.save! end end end 
0
Oct. 25 '17 at 12:11
source share



All Articles