I'm using Rails' accepts_nested_attributes_for method with great success, but how can I have it not create new records if a record already exists?
By way of example:
Say I've got three models, Team, Membership, and Player, and each team has_many players through memberships, and players can belong to many teams. The Team model might then accept nested attributes for players, but that means that each player submitted through the combined team+player(s) form will be created as a new player record.
How should I go about doing things if I want to only create a new player record this way if there isn't already a player with the same name? If there is a player with the same name, no new player records should be created, but instead the correct player should be found and associated with the new team record.
This works great if you have a has_one or belongs_to relationship. But fell short with a has_many or has_many through.
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:
Tag Objects
Solution
I did indeed override the autosave_associated_recored_for method as follows:
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.
Don't think of it as adding players to teams, think of it as adding memberships to teams. The form doesn't work with the players directly. The Membership model can have a
player_name
virtual attribute. Behind the scenes this can either look up a player or create one.And then just add a player_name text field to any Membership form builder.
This way it is not specific to accepts_nested_attributes_for and can be used in any membership form.
Note: With this technique the Player model is created before validation happens. If you don't want this effect then store the player in an instance variable and then save it in a before_save callback.
A
before_validation
hook is a good choice: it's a standard mechanism resulting in simpler code than overriding the more obscureautosave_associated_records_for_*
.Just to round things out in terms of the question (refers to find_or_create), the if block in Francois' answer could be rephrased as:
When using
:accepts_nested_attributes_for
, submitting theid
of an existing record will cause ActiveRecord to update the existing record instead of creating a new record. I'm not sure what your markup is like, but try something roughly like this:The Player name will be updated if the
id
is supplied, but created otherwise.The approach of defining
autosave_associated_record_for_
method is very interesting. I'll certainly use that! However, consider this simpler solution as well.Answer by @François Beausoleil is awesome and solved a big problem. Great to learn about the concept of
autosave_associated_record_for
.However, I found one corner case in this implementation. In case of
update
of existing post's author(A1
), if a new author name(A2
) is passed, it will end up changing the original(A1
) author's name.Oringinal code:
It is because, in case of edit,
self.author
for post will already be an author with id:1, it will go in else, block and will update thatauthor
instead of creating new one.I changed the code(
elsif
condition) to mitigate this issue: