Search code examples
ruby-on-railsrubynestedsimple-formnested-forms

How to create triple nested objects in Rails with multiple-directional associations


An article called Triple nested Forms in Rails presents a good description of creating a form for saving three nested objects. The example given is of creating a Show that has_many Seasons, and each Season has_many Episodes. Also, Episode --> belongs_to --> Season --> belongs_to --> Show.  Shows are created like this:

def new
@show = Show.new
@show.seasons.build.episodes.build
end

The form looks like this:

<%= form.fields_for :seasons do |s| %>
    <%= s.label :number %>
    <%= s.number_field :number %>    <%= s.fields_for :episodes do |e| %>
      <%= e.label :title %>
      <%= e.text_field :title %>
    <% end %> 
 <% end %>
<% end %>

This seems straightforward because all the associations run in one direction. I'm trying to do something that's similar, but more complicated. I have a Parent model where each Parent has multiple Children and each Child is enrolled in a School. After specifying that Children is the plural of Child, the association would have to be like this:

Parent has_many Children, accepts_nested_attributes_for :children
Child belongs_to Parent, belongs_to School, accepts_nested_attributes_for :school
School has_many Children, accepts_nested_attributes_for :children

Graphically, it would look like this:

Parent <-- belongs_to <-- Child --> belongs_to --> School

Each Parent is also associated with a User, like this:

User has_many :parents

The data on Parents, Children, and Schools is entered in the following form (generated using the Simple Form gem), where the schools are entered as a dropdown selector populated from the schools table:

@schools = School.all

<%= simple_form_for (@parent) do |f| %>
     <%= f.input :name, label: 'name' %>
     <%= f.simple_fields_for :children, @children do |child_form| %>
         <%= child_form.input :name, label: "Child Name" %>
         <%= child_form.simple_fields_for :school, @school do |school %>
             <%= school.collection_select :id, @schools, :id, :name, {}, {} %>
            <% end %>
     <% end %>
<% end %>

I set up the new controller method to create a Parent having three Children enrolled in an existing School. Then I tried to associate the Children with a School that already exists in the schools table with id = 1.

def new
   @parent = Parent.new
   # creating 3 children
   @children = Array.new(3) {@parent.children.build}
   @school = School.find(1)
   @school.children.build
end

This throws an error 

Couldn't find School with ID=1 for Child with ID=

The error is located in the first line of the create method, which looks like this:

def create
    @parent = Parent.new(parent_params.merge(:user => current_user))
    if @parent.save
        redirect_to root_path
    else
        render :new, :status => :unprocessable_entity
    end
end

def parent_params
    params.require(:parent).permit(:name, :child_attributes => [:id, :name, age, :school_attributes => [:id, :name]])
end

Since the error text states "Child with ID= ", the error must be thrown before ids for new Children are assigned. Why can a School with ID=1 not be found when it exists in the schools table? Or, does this mean that a School record has not been properly associated with an instance of Child before an attempt is made to save that instance? If so, how can I fix the association?


Solution

  • One of the most common missconceptions/misstakes with nested attributes is to think that you need it to do simple association assignment. You don't. You just need to pass an id to the assocation_name_id= setter.

    If fact using nested attributes won't even do what you want at all. It won't create an assocation from an existing record when you do child.school_attributes = [{ id: 1 }] rather it will attempt to create a new record or update an existing school record.

    You would only need to accept nested attributes for school if the user is creating a school at the same time. And in that case its probally a better idea to use Ajax rather than stuffing everything into one mega action.

    <%= simple_form_for (@parent) do |f| %>
      <%= f.input :name, label: 'name' %>
      <%= f.simple_fields_for :children, @children do |child_form| %>
        <%= child_form.input :name, label: "Child Name" %>
        <%= child_form.associaton :school, 
            collection: @schools, label_method: :name %>
      <% end %>
    <% end %>
    
    def parent_params
      params.require(:parent).permit( :name, 
        child_attributes: [:id, :name, :age, :school_id]]
      )
    end