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
Code Walk Through on DB Tables & Association
class Item < ActiveRecord::Base
# many-to-1 association with Order
belongs_to :order
end
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
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
class Order < ActiveRecord::Base
# Order has 1 to many assocation with items
has_many :items
end
Route
resources :orders do
resources :items
end
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
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
<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 %>
<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) %>
<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) %>
<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
<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
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
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
...
<% @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
class Order < ActiveRecord::Base
# If an order is deleted, all its "items" are deleted automatically
has_many :item, :dependent => :destroy
end
|