Yes, I understand this is going to be a weird post for some of you. Frankly, I haven’t been writing enough about what I spend most of my day doing, so here it is.
I have been having a bafflingly hard time trying to figure out the proper way to insert multiple child records from one single webform. It is the standard fare for posting things like invoice headers and details. Say for example, you’ve got an invoice record which consists of an order number, date and whatnot. That particular piece of tabular data is then considered the parent of its child line items (product_id, description, quantity, price, etc). So you’ve got this Order which consists of an invoice and line items.
It’s pretty basic, but I’ve had the hardest time figuring out how to do this is Ruby on Rails. I know it’s not hard in theory, but with Rails, since there is only One Right Way(TM) to do things, ’cause you’re on Rails, it takes a bit of doing to figure out this Right Way(TM). I personally don’t have a problem doing it Rails’ way, but please dear God, just tell me what it is. I bought the book and everything.
So here’s the dope, folks. Please correct me if there’s a better way of doing this, ’cause I’m a Rails n00b. For reference sake, I am using Ruby on Rails 1.0 with Postgres 8.0.4 and Ruby 1.8.4
First the webform:
<ul id=items>
<% for item in @items %>
<li><%= check_box_tag 'line_item', item.id, checked=false, {:name => "line_item[item_id][]", :id => "line_item_id_#{item.id}" } %><label for="line_item_id_<%= item.id %>"> <%= item.title %></label></li>
<% end %>
</ul>
Make sure to use check_box_tag instead of check_box. check_box holds a hidden text input that by default inserts a 0 into the database. From http://rubyonrails.org/api/classes/ActionView/Helpers/FormHelper.html
The checked_value defaults to 1 while the
default unchecked_value is set to 0 which is convenient for
boolean values. Usually unchecked checkboxes don’t post anything. We
work around this problem by adding a hidden value with the same name as the
checkbox.
You don’t want that. You want the plain vanilla check_box_tag which does none of that nonsense, because you in fact, don’t want your line_item table being filled with up with all kinds of line_items referring to product "0" or product "NULL".
So that’s our form. The line <% for item in @items %>
comes from the items.rb model and is just a little database query to get all the items associated with a particular order for posting in our invoice. Why would we use checkboxes? Well, maybe we’re not going to invoice the whole order. Maybe we’re out of some items. We’ll let our warehouse guy check off the checkboxes on his wireless pda. How’s that?
If you were watching closely, you’ll notice that I modified the check_box_tag behavior with options of my own with the following:
:name => "line_item[item_id][]"
This is what gives us multiple lines (an array of items) to pass to the controller. [] is the important part.
Now, so far this is easy, or at least I thought so. I’ve done this a hundred times in php, but that’s just the problem, I got tired of writing and re-writing this. I wanted Rails to handle all the parent child relationships for me and leave me alone. I’m lazy.
But I couldn’t figure out exactly how to do this. Frankly, I’m still trying to fit all the method/class/object/instance/variable blah blah blah into my head and keep all the Invoice invoice invoices straight. I know, I know, it’s probably me, but I’ll wager there are a few more slow-witted programmers out there for whom this is all so confusing. A phrase that I have been becoming more familiar with while working in Ruby on Rails is, "Use the force." It’s funny, but most of the time when I relax and make stuff up without trying to "understand," things usually Just Work(TM). Jedi Programming… who knew?
So we’ve got our form. Now we need to post the parent and the children in one fell swoop.
Now for the model (no, not Victoria’s Secret): Invoice will not reference the children (the children will come running when they hear their parent’s voice regardless of whether they are called by name). The parent "has_many" children and does not bother remembering their names or ids or anything. The children on the other hand "belong_to " (or reference) the parent and are tattooed with the parent_id stamp of ownership (big ears for example). When they are required, they will all line up under the parent and file out like good little children.
Got it? Parent -> has_many :children, Child -> belongs_to :parent – the model of a perfect Catholic Rails family.
Now we need to post the stuff. This is a snippet from the invoice_controller.rb:
def create
@invoice = Invoice.new(params[:invoice])
@invoice.order_id = @session["order_id"]
for item_id in params[:invoice_item][:item_id] do
@invoice.invoice_items << InvoiceItem.new(:item_id => item_id)
end
if @invoice.save
flash[:notice] = 'Invoice was successfully created.'
redirect_to :action => 'list'
else
render :action => 'new'
end
end
order_id is stored in the session array and is used to reference the invoice. The invoice in turn has items added to it for each item_id in the params passed from our form. What happens on @invoice.save is the following:
- Rails inserts the invoice header (the parent)
- Immediately fetches currval(invoices_id_seq) to retrieve the newly created invoice_id
- Uses that invoice_id number and iterates over the invoice_items inserting both the item_id and invoice_id
- commits the results if successful
That’s it! Easy, huh? Well it took me all day to figure it out. I knew it was easy, but perhaps I don’t have mad google sklz or something, because it left me scratching my head. Hopefully someone will find this useful. Leave a comment and I’ll do my best to answer your questions. If not, I’m sure I’ll forget it in a few months and have to reread this *G*.
You’ve just saved me about an hour of hair pulling, I needed exactly the same thing in my little accounting app. 🙂