rails 3 has_many :through record save error - ruby-on-rails

I'm not exactly sure what my problem is, so this question may require some more clarification, but here's what seems to be most relevant:
I have a has_many :through and the join model has some fields that aren't foreign keys. When I build the models up and try to save I get a validation error on the non-foreign key fields from the join model.
My files look like:
Person.rb
has_many :wedding_assignments, :dependent => :destroy
has_many :weddings, :through=>:wedding_assignments
accepts_nested_attributes_for :weddings
accepts_nested_attributes_for :wedding_assignments
Wedding.rb
has_many :wedding_assignments, :dependent => :destroy
has_many :people, :through=>:wedding_assignments
accepts_nested_attributes_for :people
accepts_nested_attributes_for :wedding_assignments
WeddingAssignment.rb
belongs_to :person
belongs_to :wedding
validates_presence_of :role, :person, :wedding
(role is a string)
people_controller.rb
def new
#person = Person.new
1.times do
wedding = #person.weddings.build
1.times do
assignment = wedding.wedding_assignments.build
assignment.person = #person
assignment.wedding = wedding
end
end
end
def create
#person = Person.new(params[:person])
#person.weddings.each do |wedding|
wedding.wedding_assignments.each do |assignment|
assignment.person = #person #i don't think I should need to set person and wedding manually, but I get a validation error if I don't
assignment.wedding = wedding
end
end
end
the params that come back look like:
{"first_name"=>"", "last_name"=>"", "weddings_attributes"=>{"0"=>{"wedding_assignments_attributes"=>{"0"=>{"role"=>"Bride's Maid", "budget"=>""}}, "date"=>"", "ceremony_notes"=>""}}}
And the exact error is:
ActiveRecord::RecordInvalid in PeopleController#create
Validation failed: Role can't be blank
Which is clearly not correct, since you can see it in params[]
What am I doing wrong?
This is rails 3.0.0

Right, this is a bit of a guess, so apologies if I wind up wasting your time here...
It looks to me like in your create method, you're creating the 'wedding' relationship (which is only a 'pretend' relationship really, has it's using :through => :wedding_assignments), and then returning this. You're then asking rails to re-create these objects in your call to Person.new. My guess is that rails is getting confused by trying to create an object at the far side of a has_many :through without the intermediate object being present.
I would be tempted to restructure this a little (untested code!):
def new
#person = Person.new
#wedding = Wedding.new
#wedding_assignment = WeddingAssignment.new
end
def create
#person = Person.new(params[:person])
#wedding = Wedding.new(params[:person])
#assignment = WeddingAssignment.new(params[:wedding_assignment].merge({:person => #person}))
end
I've got a feeling this'll work until the last line. I suspect to get that to work you might need to use transactions:
def create
#person = Person.new(params[:person])
#wedding = Wedding.new(params[:person])
ActiveRecord::Base.transaction do
if #person.valid? && #wedding.valid?
[#person,#wedding].each.save!
#assignment = WeddingAssignment.new(params[:wedding_assignment].merge({:person => #person}))
#assignment.save!
end
end
end
This ought to ensure that everything is created in the right order and IDs are available at the right times etc. Unfortunately though, it's a bit more complicated than your example, and does mean that you'll struggle to support multiple weddings.
Hope this helps, and doesn't wind up being a blind alley.

Try changing "Person.new" to "Person.create", maybe creating the record in the db right away will help with the associations.

Related

Rails Nested Form Creating Wrong Foreign Key

Fairly new to Rails and have tried a number of things here without success.
My problem is when I post to the database with this nested form, one of my tables (apartment_images) posts with the wrong foreign key (apartment_id).
I have a fairly complex model relationship: I have a Building has_many_through another table that associates it with (among others) an Apartments table. The problematic apartment_images table belongs to Apartments.
A summarized version is below:
Building Model
has_many :building_relationships
has_many :apartments, :through => :building_relationships
accepts_nested_attributes_for :apartments, allow_destroy: true
Apartment Model
belongs_to :building
has_many :apartment_images, -> { order(position: :asc) }, dependent: :destroy
has_many :building_relationships
has_many :buildings, :through => :building_relationships
ApartmentImage Model
belongs_to :apartment
buildings_controller (excluded new method)
def createNewBuilding
#building = Building.new(building_params)
#apartment = Apartment.where(building_id: #building.id)
#also tried this but results in no id being save:
##apartment = #building.apartments.build(apartment_params)
if #building.save
redirect_to newBuilding_path, notice: "Successfully created building"
else
render 'newBuilding'#, notice: "ERROR"
end
if apartment_image_params
apartment_image_params[:image].each do |value|
#apartment.apartment_images.build({image: value}).save
end
end
end
def apartment_image_params
#also tried adding :apartment_id. didn't work.
params.require(:apartment_image).permit(:id, image: []) if params[:apartment_image]
end
so I finally got it to work. Don't fully understand why this worked vs what I was doing, but will be reading up on it (but I'm also all ears if it's obvious to you guys :) ).
The answer was to switch from using a where / find / findby statement to using Rails magic:
def createNewBuilding
#building = Building.new(building_params)
#apartments = #building.apartments
#apartments.each do |apartments|

Usage of has_many relationship to save records

I am new to Rails and kind of confused by the has_one and has_many relationship.
I have two models - USER and LOCATION.
Each USER has a location and a LOCATION can belong to many USERS.
So, I created it this way:
class User < ActiveRecord::Base
belongs_to :location
end
class Location < ActiveRecord::Base
has_many :users
end
Now when I want to create a new user and save the location, how should I do it?
Is this the way ???
#user = User.new
#user.name = params[:name]
#loc = Location.new
#loc.zip = params[:zip]
#loc.save
#user.location = #loc
#user.save
This is not right and is not working for me. Any help or pointers would be appreciated.
The rails books specify only how to create these associations. Not as to how to save the records with these associations.
change save for save! to raise an exception and see what is not working for you.
An alternative way would be
#loc = Location.create!(:zip => params[:zip])
#user.create!(:name => params[:name], :location_id => #loc.id)
you might enjoy receiving something like params[:users][:name] as parameter, because for instance if there are more attributes than name you can just do
#user.create(params[:user])

Saving belongs_to data when creating new record

class Party < ActiveRecord::Base
belongs_to :hostess, class_name: 'Person', foreign_key: 'hostess_id'
validates_presence_of :hostess
end
class Person < ActiveRecord::Base
has_many :parties, foreign_key: :hostess_id
end
When creating a new Party, the view lets the user select an existing Hostess, or enter a new one. (This is done with jQuery autocomplete to look up existing records.) If an existing record is chosen, params[:party][:hostess_id] will have the correct value. Otherwise, params[:party][:hostess_id] is 0 and params[:party][:hostess] has the data to create a new Hostess (e.g., params[:party][:hostess][:first_name], etc.)
In the Parties controller:
def create
if params[:party][:hostess_id] == 0
# create new hostess record
if #hostess = Person.create!(params[:party][:hostess])
params[:party][:hostess_id] = #hostess.id
end
end
#party = Party.new(params[:party])
if #party.save
redirect_to #party, :notice => "Successfully created party."
else
#hostess = #party.build_hostess(params[:party][:hostess])
render :action => 'new'
end
end
This is working fine when I pass in an existing Hostess, but it's not working when trying to create the new Hostess (fails to create the new Hostess/Person and thus fails on creating the new Party). Any suggestions?
Given the models you provided, you can have this setup in a cleaner way using a few rails tools like inverse_of, accepts_nested_attributes_for, attr_accessor, and callbacks.
# Model
class Party < ActiveRecord::Base
belongs_to :hostess, class_name: 'Person', foreign_key: 'hostess_id', inverse_of: :parties
validates_presence_of :hostess
# Use f.fields_for :hostess in your form
accepts_nested_attributes_for :hostess
attr_accessor :hostess_id
before_validation :set_selected_hostess
private
def set_selected_hostess
if hostess_id && hostess_id != '0'
self.hostess = Hostess.find(hostess_id)
end
end
end
# Controller
def create
#party = Party.new(params[:party])
if #party.save
redirect_to #party, :notice => "Successfully created party."
else
render :action => 'new'
end
end
We're doing quite a few things here.
First of all, we're using inverse_of in the belongs_to association, which allows you to validate presence of the parent model.
Second, we're using accepts_nested_attributes_for which allows you to pass params[:party][:hostess] into the party model and let it build the hostess for you.
Third, we're setting up an attr_accessor for :hostess_id, which cleans up controller logic quite a bit, allowing the model to decide what to do whether it receives hostess object or the hostess_id value.
Fourth, we're making sure to override hostess with an existing hostess in case we got a proper hostess_id value. We do this by assigning hostess in the before_validation callback.
I didn't actually check if this code works, but hopefully it reveals enough information to solve your problem and exposes more helpful tools lurking in rails.

Rails, saving the foreign key in a `belongs_to` association

I think I'm having a really basic problem here but I can't seem to put my finger on what I'm doing wrong.
So the issue here is when I save an instance of a model the foreign_key for the models's belongs_to association (in this case the user_id is not being saved, so I'm forced to do this:
def new
#thing = Thing.new(:user_id => current_user.id)
end
def create
#thing = Thing.new(params[:thing])
#thing.user_id = current_user.id
if #thing.save
redirect_to #thing
else
render 'new'
end
end
Shouldn't the user_id get saved automatically if my model has this association?
class Thing < ActiveRecord::Base
belongs_to :user
end
The reason I'm having this issue in the first place is because the gem friendly_id has changed the way all of my ids work and now return the objects slug... pretty annoying in my opinion.
I would try #thing.user = User.find(current_user.id) instead in your controller. Have you also got the has_many :things association declared in your user model?

accepts_nested_attributes_for with find_or_create?

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.
When you define a hook for autosave associations, the normal code path is skipped and your method is called instead. Thus, 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 is untested, but it should be pretty much what you need.
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.
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 a player_name text field to any Membership form builder.
<%= f.text_field :player_name %>
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 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
When using :accepts_nested_attributes_for, submitting the id 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:
<%= text_field_tag "team[player][name]", current_player.name %>
<%= hidden_field_tag "team[player][id]", current_player.id if current_player %>
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.
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:
self.author = Author.find_or_create_by_name(author.name) unless author.name.blank?
self.author.save!
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:
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_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 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.
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.
p = Post.first
p.author #<Author id: 1, name: 'JK Rowling'>
# now edit is triggered, and new author(non existing) is passed(e.g: Cal Newport).
p.author #<Author id: 1, name: 'Cal Newport'>
Oringinal 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
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 that author instead of creating new one.
I changed the code(elsif condition) to mitigate this issue:
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
#dustin-m's answer was instrumental for me - I am doing something custom with a has_many :through relationship. I have a Topic which has one Trend, which has many children (recursive).
ActiveRecord does not like it when I configure this as a standard has_many :searches, through: trend, source: :children relationship. It retrieves topic.trend and topic.searches but won't do topic.searches.create(name: foo).
So I used the above to construct a custom autosave and am achieving 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

Resources