Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Ruby

Rails 3.2: A Nested-Form Demo, Part 1: All Wings Report In!

4.00/5 (1 vote)
24 Jan 2013CPOL8 min read 15.3K  
A nested-form demo.

Background

I've been working on a Rails 3.2 project for the last several months. While it's been a blast to learn a new technology stack (I've spent most of my career working with .NET), there have definitely been some bumps in the road. In particular, I've spent the last couple weeks working on persisting a model that has a parent-child relationship. The bulk of that time was spent poking around the inter-webs, looking for tips and tricks to accomplish my goal. During my inter-web search, I couldn't find a single post (or series of posts) that had a complete example or tutorial on how to persist a model that has a parent/child relationship. My Google-fu might not be as good as some other developers out there, but I searched pretty hard. What I found were bits and pieces scattered about (primarily on my savior, Stackoverflow). After a LOT of trial and error (mostly the latter), I finally have it working. So I thought I'd collect my notes as a series of posts. FYI -  this started as one post, but got WAY too long. Even for me.

Objective/Goals

Reading is real neat, and I do a whole lot of it. I tend to learn by doing, so reading only gets me halfway there. To that end, we'll build a demo application that ultimately addresses the following:

  • Allows a user to save a model that has a parent/child relationship.
    • The operation will allow us to save the parent object as well as insert/update/delete new child objects that are associated to the parent.
  • Handles saving a model that has a many-to-many relationship (e.g. a User might belong to many Security Groups).

The Application

Most demo apps I see deal with users, customers, security groups and the like. That's all well and good, but I thought it'd be fun to work on a demo app about something I like - Star Wars! My favorite aspect of Star Wars is the ships, particularly the fighters. So, let's build a simple database app that will allow us to maintain data about the fighters in the Star Wars universe. The idea behind the demo application is fairly simple: it's a Star Wars Starfighter Recognition Guide (albeit a VERY simplified one).

Initially, the concept for our initial application is very simple:

  1. There are Ships.
  2. Ships have zero to many Pilots (i.e. characters/personalities who fly the type of ship being viewed).

For example: Garven Dreis, Wedge Antilles, and Biggs Darklighter all fly the T-65 X-Wing.

This application will allow a user to create/edit/delete a Ship. While creating a Ship, a user has the option of creating records for the Pilots that fly the Ship being created. When editing an existing Ship, a user has the ability to:

  1. Create a new Pilot.
  2. Edit an existing Pilot.
  3. Remove a Pilot from the list.

A user can conduct any number of the changes cited above prior to saving. When the form is submitted, we expect Rails/ActiveRecord will do the right thing. It will:

  1. Destroy the Pilots that were removed.
  2. Add the Pilots that were created.
  3. Update the Pilots that were changed.
  4. Update the Ship attributes that were changed.

NOTE: This series of posts assumes some basic knowledge of Ruby and Rails. I highly recommend going through the Rails Tutorial - it's a great introduction to the technology.

Righty-o - let's get started.

The Model

For starters, let's take a look at the domain model we'll be working with for this demonstration.  We'll keep things simple by having a straightforward parent/child relationship:

  • Ship: This will be our "parent" object - it represents some basic information about a starfighter in the Star Wars universe.
  • Pilot:  This will be our "child" object - it represents a person who is rated to fly the starfighter.

Here's a picture of what our domain model we'll be working with (thanks, RubyMine!):

the Ship and Pilot model

Moving on, let's take a look at the code for each object in our domain model:

app/models/ship.rb:

class Ship < ActiveRecord::Base   
  attr_accessible :armament, :crew, :has_astromech, :name, :speed   
  attr_accessible :pilots_attributes   

  has_many :pilots   
  accepts_nested_attributes_for :pilots,                                 
                                :reject_if => lambda { |attrs| attrs.all? { |key, value| value.blank? } },
                                :allow_destroy => true

  #I find your lack of validation disturbing...
end

I'd like to call your attention to the attr_accessible :pilots_attributes - that'll be important later.

app/models/pilot.rb:

class Pilot < ActiveRecord::Base
  belongs_to :ship
  attr_accessible :call_sign, :first_name, :last_name, :ship_id

  #I find your lack of validation disturbing...
end

The accepts_nested_attributes_for Method

According to the documentation, the accepts_nested_attributes_for method "Defines an attributes writer for the specified association(s)." What does that mean, exactly? That means that our Ship model can take in attributes for any of its associated Pilots and update them. To use accepts_nested_attributes_for, we point it at one of our associations. Right now, we only have one association - has_many :pilots - so the choice is pretty easy. Armed with the accepts_nested_attributes_for :pilots, we can:

  • add new Pilots to our Ship.
  • update our Ship's existing Pilots.
  • there is a special attribute (_destroy) that will allow us to mark certain Pilots to be deleted from our Ship(more on this in a bit).

How accepts_nested_attributes_for Works

What's going on behind the scenes (as I understand it, anyway) is this: When you add an accepts_nested_attributes_for method to your model, a writer is also added to your model. That writer will be named as follows [the name of your association]_attributes. In our case, since we have a accepts_nested_attributes_for :pilots method, the writer will be named pilots_attributes (note the pluralization of Pilot).

accepts_nested_attributes_for Options

Notice that our Ship model also has an attr_accessible :pilots_attributes accessor set up. This allows us to easily pass our Pilot data to the pilots_attributes writer that was created as a result of the accepts_nested_attributes_for implementation. With the pilots_attributes in our attr_accessible list, we won't get the dreaded "Can't mass-assign" error.

NOTE: having your accepts_nested_attributes_for writer available for mass-assignment might not be a good idea for your real-life application. Take careful consideration when adding your model's attributes to the attr_accessible list.

Next, we have the :reject_if option. This option allows us to specify a method (using a symbol or an anonymous method) to determine if a Pilot record should be built. Whatever code is executed by the :reject_if option should return true or false. In our case, we don't want to build a Pilot record if all the attributes are empty. This will prevent ActiveRecord from saving blank/empty rows in our Pilots table.

Finally, we have the :allow_destroy option. This option can be set to true or false, and it does pretty much what it says. If we have :allow_destroy => true (as we do in our case), ActiveRecord can delete the Pilots that have their :_destroy attribute set to true.

You can check out the documentation and the available options for the accepts_nested_attributes_for method here.

What a POST Looks Like From the UI

I realize I'm skipping ahead a bit, 'cause we haven't even talked about the UI. However, I thought it would be beneficial to see how the data for nested objects (i.e. our Pilots) is sent across the wire. After watching a couple POSTs, it's really easy to see what's going on with all the setup we did on our models above.

Scenario 1 - A User Creates a New Ship With No Pilots

If a user only creates a Ship and no Pilots (i.e. the ships#create method is called on the ships_controller), the data contained in the POST will look like this (taken and "prettified" a bit from Firebug):

&ship[name]=TIE+Fighter &ship[crew]=1 &ship[has_astromech]=0 &ship[speed]=100 &ship[armament]=2+laser+cannons

As you can see above, we're only passing attributes for our Ship object. The controller will take these parameters and build a new Ship object. Here's the SQL that was executed when the controller saved our new Ship to the database:

 INSERT INTO "ships" ("armament", "created_at", "crew", "has_astromech",
 "name", "speed", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) 
[["armament", "2 laser cannons"], ["created_at", Tue, 15 Jan 2013 
19:04:13 UTC +00:00], ["crew", 1], ["has_astromech", false], ["name", 
"TIE Fighter"], ["speed", 100], ["updated_at", Tue, 15 Jan 2013 19:04:13
 UTC +00:00]] 

Scenario 2 - A User Creates a New Ship With a Couple Pilots

Now our user is getting a little ambitious, but thanks to the setup we did on our model we're ready. In this case, a user has created a new Ship with a couple Pilots associated. Shown below is a POST that creates a new Ship with two Pilots:

&ship[name]=TIE+Fighter &ship[crew]=1 &ship[has_astromech]=0 &ship[speed]=100 &ship[armament]=2+laser+cannons &ship[pilots_attributes][1358277455305][first_name]=Cive &ship[pilots_attributes][1358277455305][last_name]=Rashon &ship[pilots_attributes][1358277455305][call_sign]=Howlrunner &ship[pilots_attributes][1358277455305][_destroy]=false &ship[pilots_attributes][1358277658761][first_name]=Dodson &ship[pilots_attributes][1358277658761][last_name]=Makraven &ship[pilots_attributes][1358277658761][call_sign]=Night+Beast &ship[pilots_attributes][1358277658761][_destroy]=false

Let's take a look at the pilots_attributes parameters - they correspond to our :pilots_attributes accessor. Each set of pilots_attributes has four attributes grouped by a "unique id" of sorts (we'll see how the unique id is generated in a future article). In our controller, when we call Ship.new(params[:ship]), ActiveRecord can build the Pilots that correspond to the Ship in the ships#create method. That's because we have declared attr_accessible :pilots_attributes in our Ship model. We can also see the special _destroy attribute, which is set to false in both cases. That means we want to add these Pilots to our database.

For the sake of completeness, here's the create method in the ships_controller:
app/controllers/ships_controller.rb:

def create
  @ship = Ship.new(params[:ship])

  if @ship.save
    redirect_to(ships_path, :notice => "The #{ @ship.name } ship has been saved successfully.")   
  else
    render(:new, :error => @ship.errors)
  end
end

Again, here's the SQL that was generated by ActiveRecord to save our Ship and its associated Pilots:

 (0.1ms) begin transaction
 SQL (0.4ms) INSERT INTO "ships" ("armament", "created_at", "crew", 
"has_astromech", "name", "speed", "updated_at") VALUES (?, ?, ?, ?, ?, 
?, ?) [["armament", "2 laser cannons"], ["created_at", Tue, 15 Jan 2013 
19:21:33 UTC +00:00], ["crew", 1], ["has_astromech", false], ["name", 
"TIE Fighter"], ["speed", 100], ["updated_at", Tue, 15 Jan 2013 19:21:33
 UTC +00:00]]
SQL (0.7ms) INSERT INTO "pilots" ("call_sign", 
  "created_at", "first_name", "last_name", "ship_id", 
  "updated_at") VALUES (?, ?, ?, ?, ?, ?) [["call_sign", "Howlrunner"], 
  ["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], ["first_name", 
  "Cive"], ["last_name", "Rashon"], ["ship_id", 8], 
  ["updated_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00]]

SQL (0.3ms) INSERT INTO "pilots" ("call_sign", "created_at", 
  "first_name", "last_name", "ship_id", 
  "updated_at") VALUES (?, ?, ?, ?, ?, ?) [["call_sign", "Night Beast"], 
  ["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], ["first_name", 
  "Dodson"], ["last_name", "Makraven"], ["ship_id", 8], ["updated_at", 
  Tue, 15 Jan 2013 19:21:33 UTC +00:00]]
  (2.2ms) commit transaction

Summary: Look at the Size of That Thing!

We've barely impacted on the surface of our Starfighter Recognition Guide, but we've managed to take a fairly detailed look at how to:

  • Establish a parent/child relationship in our model.
  • Set up our "parent" model to accept data for any "child" objects, allowing us to save the "parent" and all its "children" at once.
      We've also used the Force to look ahead at our UI and the data it will send to our controller when the user submits the form. This peek into the future has exposed some of the internal plumbing that goes on when Rails builds/saves a model with a parent/child relationship.

In our next installment, we'll take a good long look at the controller and the UI helpers for our Starfighter Recognition Guide. We'll add some screens that will allow our user to perform CRUD operations on our Ships and Pilots.

Stay tuned... 

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)