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