Sunday, January 16, 2011

Introduction to Ruby on Rails

Introduction

I am a .NET developer and have been programming ASP.NET in C# for a number of years, I have been hearing some pretty wonderful stuff about Ruby on Rails and wanted to see for myself. This post is about my discovery of this framework from the unknown to creating an ecommerce site from scratch.

Environment

IDE: NetBeans IDE 6.8
Ruby Platform: Build-in JRuby 1.4.0
Server: GlassFish v3 Domain
Database: MySQL

Application
The application is simple and one that followers of this blog will recognise. It is a simple eCommerce shop with the data structured in to Products, Categories and Basket.

The purpose is to show how quickly an application in Rails can be created and takes you through the steps to set this up.

Prerequisite
You will have loaded in the Ruby and Rails plugin for NetBeans

Create the Project
In the NetBean IDE select File > New Project
Give your project a name e.g. ‘RailsShop’
See the environment section above for other settings.
Unselect ‘Add Rake Targets to Support App Server Development’
Database Configuration: Specify Database Information Directly
Database adapter: mysql (jdbc)
Database name: RailsShop_development (it will also create ‘_test’; ‘_production’
User name: localhost
Password xxxxxx
Install rails: 2.3.4

Create and set up the database
Right click on the project in the Project window > Run/Debug Rake Task> db:create

Add the tables
Right click on the project > Generate
Generator: model
Arguments: category name:string
Arguments: product category_id: integer name:string price:decimal url:string
Arguments: basket product_id: count:integer

Note that the model is created in the Models folder – this does not define the fields in the model, these are specified in the Database migrations folder. The database will not be updated until the database migration has been called.

The convention is to use the foreign key to be the model_id.
Primary key ids are created automatically as are time stamps.

Define the association between the models
A basket has products, edit the \models\basket.rb to:
class Basket < ActiveRecord::Base
belongs_to :product
end
and so on with the rest of the models

class Category < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :category
has_many :baskets
end

Update the database
Right click on the project > Migrate Database > To Current Version

If you get an error like this:
The driver encountered an error: com.mysql.jdbc.exceptions.MySQLSyntaxErrorException: Unknown database 'railsshop_development'
rake aborted!

The db:create failed silently in which case you will need to create the database manually – I did this in mySQL Workbench – this is a known issue and is discussed in the Knowledge base.

You should be able to update the database to the current version.

If for any reason you need to reset the database to its empty state run:

Right click on the project > Migrate Database > To Version 0 - Clear

Adding sample data
Right click on lib\tasks
Create db_load_dev_data.rake (using Other\Empty file) – these isn’t support for the .rake extension by default.
namespace :db_load_dev_data do
desc "This loads the development data."
task :seed => :environment do
require 'active_record/fixtures'
Dir.glob(RAILS_ROOT + '/db/fixtures/*.yml').each do file
base_name = File.basename(file, '.*')
#say "Loading #{base_name}..."
Fixtures.create_fixtures('db/fixtures', base_name)
end
end
desc "This drops the db, builds the db, and seeds the data."
task :reseed => [:environment, 'db:reset', 'db:seed']
end
The code reads the yml files and loads the data in the database:

Create a new fixtures folder under the \db\ folder and create a file for each table i.e. products.yml.

Note: in Files view \db\ equates to Database Migrations in the Projects view.

Right click on the project > Run/Debug Rake task… > [refresh tasks] > db_load_dev_data: seed

It is important to specify the id of using test data – this allows for the foreign keys to be specified otherwise the id will automatically generated.

Here is an example with two products:

Concorde Bikes PDM Team Replica Road Bike:
category_id: 1
name: "Concorde Bikes PDM Team Replica Road Bike"
price: 699.99
url: "concorde-bike-pdm-ind.jpg"

Kestrel Talon Tri 105 2010:
category_id: 1
name: "Kestrel Talon Tri 105 2010"
price: 147.99
url: "kestral-talon-tri-ind.jpg"

To remove the sample data right click on the application > Run/Debug Rake Task… > db_load_dev_data:reseed

Routes
To utilise shorthand like:
<%= link_to category.name, category%>

Ensure you have entries in the \Configuration\routes.rb:
map.resources :category
map.resources :product
map.resources :basket

More on this later.

Create the controllers
The scaffold generation will create controller methods for CRUD application, as we are displaying the site in read only we will not create with a scaffold.

Note, do not delete the application_controller.

RailsShop > Generate > controller > Category. Views: index, show

This creates:
\Controllers\category_controller
\Views\category\index.html.erb
\Views\category\show.html.erb


Edit the controller index – this is used to display all the categories

def index
@categories = Category.all

respond_to do format
format.html # index.html.erb
format.xml { render :xml => @categories }
end
end

Show is used when a single category is selected:

def show
@category = Category.find(params[:id])
respond_to do format
format.html # show.html.erb
format.xml { render :xml => @category }
end
end

We also need to create a controller for product and basket.

Product views: show
Basket views: index, update, delete, add_to_basket

Partial Views
As we want to show the categories, products and basket summary on the same page we need to use partial views.

In rails the partial views need to be located in the same folder as the containing page.

So for our example the Category show view is set out as:

\Views\category\_basket_list.html.erb
\Views\category\_category_list.html.erb
\Views\category\_product_list.html.erb
\Views\category\show.html.erb

As you can see the convention in rails is to name partial views with an underscore prefix.

These are included in the show.html.erb as:

<%= render :partial=>"category_list", :locals=>{:categories=>Category.find(:all)} %>
<%= render :partial=>"product_list", :locals=>{:products=>@category.products} %>
<%= render :partial=>"basket_list", :locals=>{:baskets=>Basket.find(:all)} %>

The Category.find(all) does not call the controller but gets the list of categories and sets the partial’s categories variable with the result of the find call.

The product_list is interesting to us, and really sets out as a great example of Rails at work making our lives easier:

The show.html.erb has a variable @category, if we refer back to the model definition, Category ‘has_many :products’ and this along with the fact that the database definition defines that Product has a category_id we can access the list of products from the @category variable and pass it to the partial as a products variable.

Partials can also be accessed if they are not in the same folder – the same partials are included in the \Views\product\show.html.erb like this:

<%= render :partial=>"category/basket_list", :locals=>{:baskets=>Basket.find(:all)} %>

Link to
Rails provides a nice short hand to call controllers from views to call an action the first I touched on earlier:

In the \Views\category\show.html.erb we set the products local variable in the _product_list.html.erb this is then used in a ‘for loop’ like this:

<% products.each do product %>
<div>
<%= link_to product.name, product%>
</div>
<% end %>

So the product is the instance of each iteration of the loop, so in the link_to what actually renders is: http://localhost:8080/RailsShop/product/2107859366, this will automatically call the product_controller show action passing the 2107859366 as a parameter id which is accessed like so:

def show
@product = Product.find(params[:id])
respond_to do format
format.html # show.html.erb
format.xml { render :xml => @product}
end
end
In certain cases we need to explicitly identify the controller, action and parameter we are passing, this can be seen when adding a product to the basket:

\Views\product\show.html.erb

We have access to the local variable @product the id of which is used as a parameter to the basket which is called:

<%= link_to 'Add to basket', :controller => "basket", :action => "add_to_basket", :id => @product.id%>

If we take a look at the ‘add_to_basket’ action:

def add_to_basket
#how many of this product is in the basket?
@product_id = params[:id]
@basket = Basket.find_by_product_id(@product_id)

if !@basket.nil?
@count = @basket.count + 1
@basket.update_attribute(:count, @count)
else
@basket = Basket.new
@basket.product_id = @product_id
@basket.count = 1
@basket.save
end

redirect_to :controller => 'category', :action => 'index'

end
The code above takes the id parameter, uses it to find the basket, if the basket does not exist a new one is created with a single product otherwise the current number of products is obtained, incremented and stored in the data layer using the update_attribute method.

Another call on the basket allows us to remove the product, this is done by:

def delete
@basket = Basket.find(params[:id])
@basket.destroy
redirect_to :action => 'index'
end

If we compare the last two controller actions we can see the redirect can be both explicit in describing the controller and implicit in that the index will call the Basket for the Basket delete action.

Submitting data in a form
We need to be able to modify the quantity of products in the basket, so how does it plumb together?

We call the basket index from an edit button on the _basket_list.html.erb:

<%= link_to 'Edit basket', :controller => "basket", :action => "index" %>

This calls the basket_controller index action:

def index
@baskets = Basket.all

respond_to do format
format.html # index.html.erb
format.xml { render :xml => @baskets }
end
end
This renders the \Views\basket\index.html.erb

<% @baskets.each do basket %>
<% form_for(basket) do f %>
<tr>
<td class ="productname"><%= basket.product.name %></td>
<td><%= basket.product.price %></td>
<td><%= f.text_field :count, :class => "qty" %></td>
<td><%= basket.product.price * basket.count %></td>
<td><%= f.hidden_field :id %></td>
<td><%= f.submit 'Update', :class => "updatebutton" %></td>
Note the hidden id field and form placement within the ‘for loop’, the submit button will call the basket_controller update method, more on the class settings in the style section later:

def update
@basket = Basket.find(params[:id])
@basket.update_attributes(params[:basket])
redirect_to :action => 'index'
end
And then re show the basket with the new line totals.

Making it look good
Layout

To maintain consistency across the site we need to create a page template. We do this by creating a new erb in \app\views\layouts\ - as ever the naming conversion is important here. We can either create one for the site named ‘application.html.erb’ or for each controller i.e. ‘category.html.erb’ the <%= yield %>is used on the page to donate the location to load the views.

Style
We set up styling in a CSS file in \public\stylesheets\default.css – this is referred to in the layout page as:

<%= stylesheet_link_tag 'default.css' %>

If we want to style the delete link in the basket and add a confirmation question we do the following:

<%= link_to 'x', {:controller => "basket", :action => "delete", :id => basket.id}, :class => "delete", :confirm => "Are you sure?"%>

Note the curly parenthesis; without it the class and confirm will be added to the link URL.

Images
Load images on the page using the image_tag, this automatically looks up images in the \public\images folder in the web application.

<%= image_tag(@product.url) %>
Run the application
I want the first page to load with the categories displayed; this is done by adding the following to \Configuration\routes.rb:

map.root :controller => "category"
To run the application from NetBeans:

Right click on the Project > Run

http://localhost:8080/RailsShop/category/

Conclusion
Firstly the structure is all about creating a real world environment the segregation between Development, Test and Production make sense.

When changes are made to the database it is easy to rollback changes and recreates the tables using the rake tasks.

The fields are not specified in the models only in the database migrations, this allows for encapsulation in code from the data layer.

I like the convention over configuration methodology; this has saved time creating rules for each instance.

So in summary then I like rails, it has a very structured approach to development and database schema changes as you can see things can be developed quickly without code bloat using the DRY methods.

I have uploaded the code to GitHub.

1 comment: