Saving nested models in Rails

Very often, I would say almost always, you have nested models in a Web application.

In  Rails, you (most often) specify it using has_many and belongs_to in the respective models.

In my example, I will take the trusty old Order and OrderItem examples.

Model

class Order < ApplicationRecord
  has_many :order_items, dependent: :destroy
  accept_nested_attributes_for :order_items, allow_destroy: true
end

The bold text means that when saving an Order, it’s possible to also save OrderItems. Rails has all the information to make this possible. The allow_destroy option means that an extra attribute _destroy is added to each OrderItem, and if set in the UI Rails will delete that item and remove it from the Order.

class OrderItem < ApplicationRecord
  belongs_to :order
end

“belongs_to” add for example a method “order” to the OrderItem so that you easily can refer to the order it belongs to. For example like this:

order.customer_id

That way, you won’t have to look up the order using the order_id you have in OrderItem.

The alternative would be to look up the Order manually using the FK order_id.

order = Order.find(order_id)

Controller

The controller should stay fairly unchanged. But since you specified that Order now accepts nested parameters for a number of OrderItems, you must permit these parameters.

def order_params
  params.require(:order).permit(
      :customer_id,
      :name,
      order_items_attributes: [
          :id,
          :order_id,
          :product_id,
          :qty,
          :_destroy
      ])
end

Note that Order.id is not “permit”

Views

The View is perhaps the trickiest part. But follow this convention.

Start with a form_with statement.

<%= form_with model: [current_user, @order], :local => true do |frm| %>
  <table>
    <%= frm.fields_for :order_items do |ff| %>
        <tr>

<!-- NOTE that "object" points to the order_item we are processing 
     could be handy if you need to do some processing --> 
          <td><%= ff.object.customer.name %></td>

          <!-- Here we deal with OrderItem fields -->
          <td><%= ff.text_field :product_id %></td>
          <td><%= ff.text_field :qty %></td>
        </tr>
    <% end %>
  </table>

  <%= frm.submit "Save" %>
<% end %>

Note a couple of things:

[current_user, @order]

since the order is nested within the current user (I’m using devise)

:local => true

otherwise it will use Ajax/JS

object

can optionally be used to access the OrderItem within the fields_for

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.