Ruby on Rails – Insert Multiple Child Records

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:
  1. Rails inserts the invoice header (the parent)
  2. Immediately fetches currval(invoices_id_seq) to retrieve the newly created invoice_id
  3. Uses that invoice_id number and iterates over the invoice_items inserting both the item_id and invoice_id
  4. 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 may also like...

20 Responses

  1. Marcin says:

    Hi

    Thanks for sharing. It helped me think about the problem. I did it slightly differently, where I didn’t need the array passing like you did. My problem was to assign metadata to an item of content – the metadata is essentially defined in a different table and there could be N metadata fields.
    After doing the lookup for the metadata in the model, i then generated a text field with a name_number based on the metadata id.

    note that I’m very new to ruby/rails (ie printed out the OnLAMP rails tutorial on 2 days ago) so i may be doing it wrong, but it seems to work. (I spent over 1 hour solving the problem of having to put the {} inside the () in the .new call for a rails data class!!)

    View fragment

    Metadata:

    MetadataValue

    ” name=”contentmetadata[]” size=”30″ type=”text” value=”” />

    Controller fragment
    def create
    @content = Content.new(params[:content])
    @contentmetadatas = params[:contentmetadata]

    if @content.save

    @contentmetadatas.each do |key,val|
    @cm = Contentmetadata.new({“content_id” => @content.id, “metadata_id” => key, “value” => val}).save if val != “”

    end

    flash[:notice] = ‘Content saved.’
    redirect_to :action => ‘list’
    else
    render :action => ‘new’
    end
    end

  2. Marcin says:

    Try again – sorry, forgot to put code marks around the html
    again, the view:

    Metadata:

    MetadataValue

    " name="contentmetadata[]" size="30" type="text" value="" />

  3. O'Malley says:

    Cool, thanks for sharing. I’m glad it was useful.

    This Ruby on Rails stuff is cool, but if it has one drawback it’s that those of us who are new to Ruby can spend hours tripping over the most basic things. At least I know I do. Usually, it’s so simple, that I kick myself for days, then promptly forget what I learned. *chuckle*.

    That’s why I have to write this stuff down.

  4. Azrael says:

    I did this in a similiar way, but i ran into problems..
    How do you display Errors?
    You dont even use a flash-notice if the insert fails…

    When inserting one by one i can display an error by redirecting to the form having this line on top.
    That doesnt work with multiple inserts, does it?

    Did you solve that problem?

  5. O'Malley says:

    What problems did you have. If for some reason the post fails, I suppose a

    flash[:notice] = ‘Invoice failed to be created.’

    in the else area could be useful.

    And then display the flash message on whatever the return page would be (in this case “new”). I don’t see it as essential however, as I’ve validated all my inputs before the save step (maybe you are having upstream problems).

    At the moment I’m saving, all my ducks are in a row and I don’t have to worry much. So, yeah, sure maybe I should check on the last step too, but I’m not too worried about it.

    Now this is all set up on ROR 1.0 so I don’t know if 1.1 introduces a new way of doing this or breaks it or anything. I’ve not checked that yet, so YMMV.

  6. chuck says:

    Hmmm… Non-transactional updates always scare me. What issues occur if the content is saved but none or only half of the contentmetadata is saved?

  7. chuck says:

    Better yet, suppose that the inserts are executing *really* slowly. Suppose the user opens up a new window and retrieves the content object, edits the partially stored metadata, and stores the content+metadata back to the database? Now you have a content record with a mixture of old and new metadata.

    How come ruby programmers don’t seem to care about transactions? Do web programmers simply not understand the concept of transactions?

  8. chuck says:

    My comments above are directed to Marcin. Here’s one for O’Malley.

    Yes, this code is very interesting to newbs like me. It shows a mechanism to “use the force” to obtain a single transaction.

    But, let’s make this a bit more difficult. Suppose you have children whose parents can be different kinds of objects. For example, consider the “is_taggable” relationship. Suppose we have Photos and URLs and Tags as tables. Then Photos have Tags and URLs have Tags. Each tag belongs to either a photo or a URL.

    I suppose we could set up separate tags tables, one for photos and one for urls, but that seems like we would replicate a lot of code.

    We could try a three tier approach with Photos, URLs, Tags and join tables for Photos-Tags and URL-Tags, but this basically gets us back to the problem that we cannot model the has-many/belongs-to relationships fully between a couple of the tiers.

    Is specifying the “belongs-to” relationship in the model fully necessary? Will the parent try to save the children if the children don’t specify “belongs to”?

    Does the “belongs-to” relationship work robustly? If we come in later and build an invoice with a different set of invoice-items, will this delete the old list of invoice-items and insert the new list?

  9. whoever says:

    “Do web programmers simply not understand the concept of transactions?”

    I think most PHP/MySQL people don’t.. Change the controller to something like this, and you’re in the clear:


    def create
    Invoice.transaction do # start transaction. any AR class would do, it applies to the connection
    @invoice = Invoice.new(params[:invoice])
    @invoice.order_id = @session["order_id"]
    for item_id in params[:invoice_item][:item_id] do
    @invoice.invoice_items item_id)
    end
    end # end transaction
    if @invoice.save
    flash[:notice] = ‘Invoice was successfully created.’
    redirect_to :action => ‘list’
    else
    render :action => ‘new’
    end
    end

  10. O'Malley says:

    Thanks for the tip, whoever (if that is your real name 🙂 ) Active Record is indeed an amazing thing.

    Thanks for the heads up on transaction

  11. Florian says:

    If you do STI you could possibliy say:
    @invoice.invoice_items = InvoiceItems.find params[:invoice_items]
    Wherefor you tell your InvoiceItem Model:
    class InvoiceItem
    And finally change the name of your checkbox tag:
    <%= check_box_tag ‘line_item’, item.id, checked=false, {:name => "invoice_items[]", :id => "line_item_id_#{item.id}" } %>

  12. Florian says:

    Ahh, I hate commentsystems without preview because I’m to lazy to read my source in this tiny textbox…

    -class InvoiceItem
    +class InvoiceItem < Item

  13. mahesh says:

    hai
    thank u for giving the detailed explation on this, but when i done the same as u did its geeting an error. i am trying for three days please help me

    Errorundefined method `order_id=’ for #

  14. varma says:

    in rhtml—

    “line_cartitem[cartitem_id][]”, :id => “line_cartitem_id_#{Product.id}”} %>
    in controller—
    def select
    @product = Product.new(params[:product])
    @product.order_id = @session[“order_id”]
    for cartitem_id in params[:product_cartitem][:cartitem_id] do
    @product.product_cartitems cartitem_id)
    end
    if @product.save
    flash[:notice] = ‘selected items was saved’
    redirect_to :action => ‘add_to_cart’
    else
    render :action => ‘index’
    end
    end

    i am geeting error as
    undefined method `order_id=’ for #

  15. ravvuinn says:

    Hey! Almost informative for my purpose…. was wondering if you know how to do the following:

    build a quiz question with five multiple choice answers, using checkboxes to pass the correct answer_id into the correct answer_id column in the questions table.

  16. O'Malley says:

    Why yes I do. Here’s a hint. Checkboxes are not what you need. Try radio.

  17. Rohit says:

    Hi,

    I want to insert multiple rows at once in the database. My functionality is to add product prices against different quantities. I have a table named productprices in the database. Now I require to insert product_id, quantity and price in the table as number of times as there are quantities corresponding to the particular product but not able to do so.

    Can you tell me how to implement this functionality.

    My view is:

    <input id = “quantity” name=”productprice[quantity]” type=”text” value=””>
    <input id = “price” name=”productprice[price]” type=”text” value=”” />

    But i am not able to access these quantities and prices in controller.

    Please help me in implementing the functionality.

  18. O'Malley says:

    ­­The relevant portion of the code is this line above:

    @invoice.invoice_items < < InvoiceItem.new(:item_id => item_id)

    From http://corelib.rubyonrails.org/classes/Array.html#M000405 :

    Append—Pushes the given object on to the end of this array. This expression returns the array itself, so several appends may be chained together.

    The "<<" is the aggregate method which allows you to append multiple rows to your invoice.invoice_items object.

    This little bit I wrote is probably out of date, so your mileage may vary. I’d check a more definitive source at one of the ruby on rails community sites or http://api.rubyonrails.com/­

  19. shridevi says:

    Thanks for explaining the difference between check_box and check_box_tag my part of my problem got solved,but when I am checking more than one box only one id is getting saved in the join table.Please help me hres the code I am trying

    html

    controller
    params[:o].collect do |os_id|

    @complist=CompanyListingsOperatingSystems.new(:company_listing_id=>@a,:operating_system_id=>os_id) if os_id != nil
    @complist.save

  1. March 9, 2006

    […] Aus einem Ruby on Rails Howto. […]