Ruby on Rails 3 Model (1-to-Many Association)

Build a 1-to-Many Association (Order has 0 to many items)

MVC Components

Order has a 1-to-many association with Item

Scaffold an Order MVC

rails generate scaffold Order order_name:string

Create an Item model (create the model only)

 rails generate model Item item_name:string order:references
  • "order:references" creates a foreign key column (order_id) in "items" referencing "orders"

"rails generate model Item" creates

In Directory Files Created Description
app/models item.rb Item Model
db/migrate 20091003043755_create_items.rb Scripts to create DB table "items"
test/unit item_test.rb Item unit testing
test/fixtures items.yml Test fixture for Item

Generate controller for Items

rails generate controller Items index show new edit

"generate controller Items" creates

In Directory File Description
app/controllers items_controller.rb Item Controller
app/views/items index.html.erb View to show all items
  show.html.erb View to show an item
  new.html.erb View to edit a new item
  edit.html.erb View to edit an existing item
app/helpers items_helper.rb Items view helper
test/unit/helpers items_helper_test.rb Items unit test
test/functional items_controller_test.rb Items functional test

Migrate the DB table

rake db:migrate

Code Walk Through on DB Tables & Association

Generated Model app/models/item.rb
class Item < ActiveRecord::Base
  # many-to-1 association with Order
  belongs_to :order
end
Generated DB Migration db/migrate/20091003043755_create_items.rb
class CreateItems < ActiveRecord::Migration
  def self.up
    create_table :items do |t|
      # Column item_name of type string
      t.string :item_name

      # Define a foreign key to order
      t.references :order

      # Timestamps on time of creation & time of update
      t.timestamps
    end
  end

  def self.down
    drop_table :items
  end
end
items table columns
mysql> describe items;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| item_name  | varchar(255) | YES  |     | NULL    |                |
| order_id   | int(11)      | YES  |     | NULL    |                |
| created_at | datetime     | YES  |     | NULL    |                |
| updated_at | datetime     | YES  |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+

Making Code Changes

Model

Add 1-to-many association to Order

app/models/order.rb
class Order < ActiveRecord::Base
  # Order has 1 to many assocation with items
  has_many :items
end

Route

config/routes.rb
  resources :orders do
    resources :items
  end
Routing Information
         Helper METHOD URL                              Map to Controller/Action
         orders GET    /orders                          {:controller=>"orders", :action=>"index"}
                POST   /orders                          {:controller=>"orders", :action=>"create"}
      new_order GET    /orders/new                      {:controller=>"orders", :action=>"new"}
     edit_order GET    /orders/:id/edit                 {:controller=>"orders", :action=>"edit"}
          order GET    /orders/:id                      {:controller=>"orders", :action=>"show"}
                PUT    /orders/:id                      {:controller=>"orders", :action=>"update"}
                DELETE /orders/:id                      {:controller=>"orders", :action=>"destroy"}
    order_items GET    /orders/:order_id/items          {:controller=>"items", :action=>"index"}
                POST   /orders/:order_id/items          {:controller=>"items", :action=>"create"}
 new_order_item GET    /orders/:order_id/items/new      {:controller=>"items", :action=>"new"}
edit_order_item GET    /orders/:order_id/items/:id/edit {:controller=>"items", :action=>"edit"}
     order_item GET    /orders/:order_id/items/:id      {:controller=>"items", :action=>"show"}
                PUT    /orders/:order_id/items/:id      {:controller=>"items", :action=>"update"}
                DELETE /orders/:order_id/items/:id      {:controller=>"items", :action=>"destroy"}

Items Controller

Add code in the Items Controller

app/controllers/items_controller.rb
class ItemsController < ApplicationController

  # GET /orders/1/items
  def index
    # For URL like /orders/1/items
    # Get the order with id=1
    @order = Order.find(params[:order_id])

    # Access all items for that order
    @items = @order.items
  end

  # GET /orders/1/items/2
  def show
    @order = Order.find(params[:order_id])

    # For URL like /orders/1/items/2
    # Find an item in orders 1 that has id=2
    @item = @order.items.find(params[:id])
  end

  # GET /orders/1/items/new
  def new
    @order = Order.find(params[:order_id])


    # Associate an item object with order 1
    @item = @order.items.build
  end

  # POST /orders/1/items
  def create
    @order = Order.find(params[:order_id])

    # For URL like /orders/1/items
    # Populate an item associate with order 1 with form data
    # Order will be associated with the item
    @item = @order.items.build(params[:item])
    if @item.save
      # Save the item successfully
      redirect_to order_item_url(@order, @item)
    else
      render :action => "new"
    end
  end

  # GET /orders/1/items/2/edit
  def edit
    @order = Order.find(params[:order_id])

    # For URL like /orders/1/items/2/edit
    # Get item id=2 for order 1
    @item = @order.items.find(params[:id])
  end

  # PUT /orders/1/items/2
  def update
    @order = Order.find(params[:order_id])
    @item = Item.find(params[:id])
    if @item.update_attributes(params[:item])
      # Save the item successfully
      redirect_to order_item_url(@order, @item)
    else
      render :action => "edit"
    end
  end

  # DELETE /orders/1/items/2
  def destroy
    @order = Order.find(params[:order_id])
    @item = Item.find(params[:id])
    @item.destroy

    respond_to do |format|
      format.html { redirect_to order_items_path(@order) }
      format.xml  { head :ok }
    end
  end

end

Add Views for Items

Items View

app/views/items/index.html.erb
<h1>Items in <%= @order.order_name %></h1>

<table>
  <tr>
    <th>Item name</th>
  </tr>

<% for item in @items %>
  <tr>
    <td><%= item.item_name %></td>
    <td><%= link_to 'Show', order_item_path(@order, item) %></td>
    <td><%= link_to 'Edit', edit_order_item_path(@order, item) %></td>
    <td><%= link_to 'Destroy', order_item_path(@order, item), :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

<br />

<%= link_to 'New item', new_order_item_path(@order) %>
<%= link_to 'Back to Order', @order %>
app/views/items/show.html.erb
<h1>Item in <%= @order.order_name %></h1>

<p>
  <b>Item name:</b>
  <%= @item.item_name %>
</p>


<%= link_to 'Edit', edit_order_item_path(@order, @item) %> |
<%= link_to 'Back', order_items_path(@order) %>
app/views/items/new.html.erb
<h1>New item</h1>

<% form_for([@order, @item]) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :item_name %><br />
    <%= f.text_field :item_name %>
  </p>
  <p>
    <%= f.submit "Create" %>
  </p>
<% end %>

<%= link_to 'Back', order_items_path(@order) %>
app/views/items/edit.html.erb
<h1>Editing item</h1>

<% form_for([@order, @item]) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :item_name %><br />
    <%= f.text_field :item_name %>
  </p>
  <p>
    <%= f.submit "Update" %>
  </p>
<% end %>

<%= link_to 'Show', order_item_path(@order, @item) %> |
<%= link_to 'Back', order_items_path(@order) %>

Order View

Change order's show.html.erb to include items information

app/views/orders/show.html.erb
<p>
  <b>Order name:</b>
  <%= @order.order_name %>
</p>

<!-- Display items of an order -->
<h2>Items</h2>
<% @order.items.each do |item| %>
  <p>
    <b>Item name:</b>
    <%= item.item_name %>
  </p>
<% end %>


<%= link_to 'Edit', edit_order_path(@order) %> |
<%= link_to 'Back', orders_path %> |
<%= link_to 'Show Items', order_items_path(@order) %>

Access Order from Item

item.order

URL Links & Redirect

link_to

link_to HTTP Method Target
<%= link_to 'Back', order_items_path(@order) %> GET /orders/1/items
<%= link_to 'Show', order_item_path(@order, @item) %> GET /orders/1/items/4
<%= link_to 'Edit', edit_order_item_path(@order, item) %> GET /orders/1/items/4/edit
<%= link_to 'Destroy', order_item_path(@order, item), :method => :delete %> DELETE /orders/1/items
<%= link_to 'New item', new_order_item_path(@order) %> POST /orders/1/items/4
<%= link_to 'Back to Order', @order %> GET /orders/1

Redirect in a controller

Call in controller Target
redirect_to order_item_url(@order, @item) /order/1/items/4
redirect_to order_items_path(@order) /order/1

Multi-Model Form with Nested Attributes

Order and item are created and edited in separate forms. Multi-Model Form supports both models in a 1-to-many association to be edited/submitted in a single form

Nested attributes can save attributes on associated objects through the parent. (Default off) Use accepts_nested_attributes_for to turn it on

Prepare the Coupons Model with a many-to-1 association with an order

rails generate model Coupon coupon_name:string order:references
rake db:migrate

Code Changes

app/models/order.rb
class Order < ActiveRecord::Base
  has_many :items

  # Order has 1 to many assocation with coupons
  has_many :coupons

  # Add coupons as nested attribute to order
  # ":allow_destroy => :true" allows an edit form to remove a coupon
  # ":reject_if" rejects saving the form data if no attributes are entered in the coupon form
  accepts_nested_attributes_for :coupons, :allow_destroy => :true  ,
    :reject_if => proc { |attrs| attrs.all? { |key, value| value.blank? } }

end

Edit views allowing coupon information to be edited in place in the order form

app/views/orders/edit.html.erb & app/views/orders/new.html.erb
...
<% @order.coupons.build if @order.coupons.empty? %>

<% form_for(@order) do |f| %>
  <%= f.error_messages %>

  <p>
    <%= f.label :order_name %><br />
    <%= f.text_field :order_name %>
  </p>

  <h2>Coupons</h2>
  <% f.fields_for :coupons do |f2| %>
    <p>
      <%= f2.label :coupon_name, 'Coupon:' %>
      <%= f2.text_field :coupon_name %>
    </p>
    <% unless f2.object.nil? || f2.object.new_record? %>
      <p>
        <%= f2.label :_delete, 'Remove:' %>
        <%= f2.check_box :_delete %>
      </p>
    <% end %>
  <% end %>

  <p>
    <%= f.submit 'Create' %>
  </p>
<% end %>

...

Create order/coupons inside a controller

# Params passed to a controller
params = { :order => { :name => '2-1 Orders', :coupons_attributes => { :coupon_name => '10% discount' } } }

Create a new order and coupons

Order.create(params[:order])

Update a coupon

params = { :order => { :coupon_attributes => { :id => '2', :coupon_name => '15% discount' } } }
order.update_attributes params[:order]

Cascade delete

When cascade delete is enabled, the deletion of a row in "orders" triggers the deletion of its associated object "items" also

Edit the model to enable cascade delete

app/models/order.rb
class Order < ActiveRecord::Base
  # If an order is deleted, all its "items" are deleted automatically
  has_many :item, :dependent => :destroy
end