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