Ruby on Rails the hard way


Table of Contents

Introduction
Scope of Rails
Concepts
Model
Controller
View
Relation between concepts
Brief "Hello World"
Adding the Model, and a form
Books with authors
Updating the view
RESTful” development
How does it all work
What about POST, PUT, DELETE etc and format_for?
Making things pretty

Introduction

There are lots of introductions to Ruby on Rails, including animated and narated demonstrations. These are very impressive at communicating the power of Ruby on Rails, but leave out a lot of details. This document sets out to explain the working of Rails in a way suitable for died in the wool programmers.

[Warning]Warning

I'm writing this document to teach myself Rails, so it might be very wrong in places!

Scope of Rails

Rails simultaneously offers heaps of functionality, but also does not take over your life. Compared to say, Plone or Zope, Rails leaves the programmer a lot more freedom, but this also means there are things you still need to do yourself.

Compared to PHP, which basically has the programmer talking directly to the browser, Rails takes care of a lot of important things, but basically does the same thing, just hidden behind the curtain.

Concepts

Like many other programs, Rails uses the terms “Model”, “View” and “Controller”. Although the exact meanings of these words is often assumed, we'll explain them here again, as they are vital to understanding the workings of Rails.

Model

A Model is something, like an order, a bill, a product, a comment etc. Models have certain attributes, like perhaps price or availability. In Rails, a Model is the central repository for the rules regarding a model.

Rules might be, for example, that the price of a product is a number, and perhaps additionally that the price is non-zero. For a comment, a rule might be that a the content of a comment is not empty.

Models are also related to each other. For example, a Comment will belong to a User, but also to a Forum. In rails speak, the Comment 'belongs_to' the User, and 'belongs_to' the Forum, while the Forum 'has_many' Comments, as does the User.

The model encompasses all constraints of something, but importantly does not contain an exhaustive description of all relevant data. Initially, a Model is empty, without rules.

Controller

Controllers operate, generally, on Models. A controler has methods, so for example, the 'UserAdmin' Controller might have a method called 'new_user', which would create a new user.

Controllers have some rules themselves called 'filters', which mean for example that 'delete_user' might be restricted to users logged in with administrative privileges. The controler then takes care of guaranteeing this condition.

View

Whenever a Controller method has done its thing, it wants to show output. Each method either has each own View, or it redirects to another method which does have its own View. It could also call 'render :text directly', a sort of inline view.

A View is a 'jsp/php' style mix of markup and content. Ruby code is embedded within either <% and %>, or if its output needs to become part of the View, between <%= and %>.

It is common to have the controller do the heavy lifting, and only have tiny snippets of Ruby in a View.

Furthermore, a View can easily link to methods within either its own or other Controllers, either directly or via the posting of a form or with Javascript code, for example when creating an 'Ajax' site.

Relation between concepts

Model, Controller and View are linked together in many ways. For example, as stated above, each Controller needs a View somewhere, so as to output the results of what it has done.

Additionally, if a Model finds that its constraints are being violated, for example if the name of a new user is empty, it will flag this error in a way that makes the Controller and Viewer print out which fields should be filled out.

So, in one paragraph, we have Models that reflect real life things like "users", "orders" or "books", then we have Controllers that operate on such Models, and these Controllers display the result of the operations using a View. Views in turn can contain links that allow the operator to execute methods within Controllers, for example to order yet another book.

Brief "Hello World"

Ruby on Rails operations are typically spread out over many files, and our 'Hello World' program indeed takes 3 files:

Table 1. Hello, world

app/controllers/say_controller.rb app/views/say/index.rhtml app/views/say/goodbye.rhtml
class SayController < ApplicationController
   def index
      @hello="Hello, world"
   end

   def goodbye
   end
end
		  
<html>
<body>
<h2>      <%= @hello %> </h2>
Hi! Click here to say 
<%= link_to('goodbye', 
	{:action=>'goodbye'}, 
	 :confirm=>'Sure you want to say goodbye?') 
%>
</body>
</html>
<html>
<body>
<h2> Goodbye!</h2>
Bye!
<p>
<%= link_to('Start over', :action => 'index') %>
</body>
</html>

What we see above is one Controller named "Say", so in RoR-parlance, its object is called the SayController, and it derives from the base ApplicationController.

This is followed by two Views, one for the "index" method of the SayController, one for its "goodbye" method. Note how the name of the controller is part of the path to the Views.

Within the Controller, note that the goodbye method is completely empty, and the index method nearly so. All it does is assign the string “Hello, world” to the @hello variable.

In the index view, we then print this @hello variable on the third line. On lines 5 and further, we find a tiny bit of Rails embedded, the 'link_to' function. In this case it says that the word 'goodbye' will be emitted, and that it will link to the 'goodbye' method. Interestingly, clicking on the link will require confirmation. Rails takes care of Javascript details.

Finally, the 'goodbye' page links back to the index.

This "Hello World" demonstration goes a long way to taking away some of the magic of Rails. Basically, we've taken a very longwinded way to link two html files, but some of the power of Rails already shines through the 'link_to' :confirmation field. RoR is rife with helpers like these.

Adding the Model, and a form

If the previous "Hello, World" demonstration is abundantly clear, it is now time to add a Model to the equation, and a form as well. Once these steps have been taken, you've basically seen all there is to Ruby on Rails, the rest is "sugar", even if there is lots of it.

We'll model a collection of books, and each book will have an author, a title and a number of pages. Later we'll add some more information. In RoR, a Model is actually two things, an 'ActiveRecord::Base', and a row in a table in a database.

This table is constructed using a 'migration' event. Migrations reflect the reality that websites and their needs keep changing, and you want to be able to go forwards and backwards through this process.

Additionally, on the Ruby side of things, a Model is derived from the 'ActiveRecord::Base', and this is where the constraints go. For us this means that we won't allow a book not to have a title or an author. Additionally, it needs a number of pages, and this needs to be a number.

Model and Migration together look like this:

Table 2. Book model

app/models/book.rbdb/migrate/001_create_books.rb
class Book < ActiveRecord::Base
        validates_presence_of :title, :author
        validates_numericality_of :pages
end
		  
class CreateBooks < ActiveRecord::Migration
  def self.up
    create_table :books do |t|
        t.column :title, :string
        t.column :author, :string
        t.column :pages, :decimal
    end
  end

  def self.down
    drop_table :books
  end
end
		  

Note that there is some area of overlap between database constraints and Model constraints. For example, we've encoded 'pages' as a ':decimal' in the database, and we also asserted the numericality of 'pages' in the Model.

To get this stuff in the database, run rake db:migrate. This document will not explain in depth how to setup database connectivity, except to point you at config/database.yml, and hint that the database needs to have the name of your 'rails' project, with '_development' appended to it. Another thing you might need to do is set the database_socket in the database.yml file.

Assuming everying is in the datbase, we can now create and store our book Model. Rails helpfully comes with a 'Console' that allows us to manipulate our project without having to use our browser.

If you run script/console, try the following:

$ script/console
Loading development environment.
>> Book.count
=> 0
>> book = Book.new(:title=> 'Ruby on Rails the Hard Way', :author=> 'Bert Hubert', :pages => 15)
=> #<Book:0xb765991c @new_record=true, @attributes={"title"=>"Ruby on Rails the Hard Way", "author"=>"Bert Hubert", "pages"=>15}>
>> book.save
=> true
>> Book.count
=> 1
>> Book.find(:all)
=> [#<Book:0xb763b4f8 @attributes={"title"=>"Ruby on Rails the Hard Way", "author"=>"Bert Hubert", "id"=>"2", "pages"=>"15"}>]
>> Book.destroy(Book.find(:all))
=> [#<Book:0xb7632dd0 @attributes={"title"=>"Ruby on Rails the Hard Way", "author"=>"Bert Hubert", "id"=>"2", "pages"=>"15"}>]
>> Book.count
=> 0
	

Here we first count the number of books in the database, and it is 0, as expected. Then we create a new Book instance, and call it book. To save it, we call its 'save' method, after which the count of Books has gone up to 1.

The next line finds ':all' books, after which we destroy all those books, and the count returns to 0.

The following bits show what happens when we violate constraints:

>> book = Book.new(:title=> 'Ruby on Rails the Hard Way', :pages=>12)
=> #<Book:0xb75d2520 @new_record=true, @attributes={"title"=>"Ruby on Rails the Hard Way", "author"=>nil, "pages"=>12}>
>> book.save
=> false
>> book.save!
ActiveRecord::RecordInvalid: Validation failed: Author can't be blank
        from /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4.5618/lib/active_record/validations.rb:763:in `save_without_transactions!'
        from /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4.5618/lib/active_record/transactions.rb:133:in `save!'
...
>> Book.create(:title=> 'Ruby on Rails the Hard Way', :pages=>12)
=> #<Book:0xb75af930 @errors=#<ActiveRecord::Errors:0xb75ad3b0 @errors={"author"=>["can't be blank"]}, 
	  @base=#<Book:0xb75af930 ...>>, @new_record=true, 
	  @attributes={"title"=>"Ruby on Rails the Hard Way", "author"=>nil, "pages"=>12}>
      

Here we find that we can generate Book instances that violate constraints, but we can't save them. The first 'save' just fails, but if we use the 'save!' method, we get an exception that tells us we've left the Author field blank.

The following line shows us the 'create' method, which tries to create a book, and save it immediately, and it too throws an exception. We can see from the '@errors' field that the errors are split out per field, which is later used by the form helpers in the View to generate good looking errors.

Hooking up the form

At this point, RoR tutorials typically create a 'scaffold'. Scaffolds are wonderful things that generate heaps of functionality for you, and allow you to manage a model without actually writing a line of code.

This is very powerful, but does little to enhance understanding of what happens under the hood.

So we'll be baring it, and writing our own model to manage Books.

We'll make a single controller called 'manager', and it will have two methods 'show_books' and 'add_book', with the 'show_books' one having a form that sends new books to the 'add_book' method.

Table 3. Book controller and forms

app/controllers/manager_controller.rbapp/views/manager/show_books.rhtml
class ManagerController < ApplicationController
        def index
                redirect_to :action => :show_books
        end

        def show_books
        end

        def add_book
                @book = Book.new(params[:book])
                if @book.save
                        redirect_to :action => :show_books
                else
                        render :action => :show_books
                end
        end
end
		    
<html>
<body>
<%= error_messages_for 'book' %>
<table border=1>
        <tr><td>Title</td><td>Author</td><td>Pages</td></tr>
        <% for item in Book.find(:all) %>
        <tr>      <td><%= h(item.title) %></td>
                <td> <%= h(item.author) %> </td>
                <td> <%= h(item.pages) %> </td> </tr>
        <% end %>

<% form_for :book, :url => { :action => :add_book} do |form| %>
        <tr>
                <td>
                        <%= form.text_field :title, :size => 40 %>
                </td>
                <td>
                        <%= form.text_field :author, :size => 40 %>
                </td>
                <td>
                        <%= form.text_field :pages, :size => 4 %>
                </td>
        </tr>
</table>
<%= submit_tag "Add book" , :class => "submit" %>
<% end %>
</body>
</html>

		    

The two files you see above make an application that shows a list of all books entered, with the last row of the table a form to enter new books. Under the table is a submit button, that submits a new book to the 'add_book' method.

In the view, pay special attention to the 'form_for' bit. This is where rather a lot of magic happens. 'form_for' actually forms a Ruby 'block', hence the <% end %> below. Within this block, we can use calls to 'form.text_field' to create input blocks that correspond to the 'Book' model. Note that we told 'form_for' that this form would be for a Book, so it knows.

On the controller side in the 'add_book' method, the contents of the form are handed *directly* to the Book constructor, using 'params[:book]', from which it takes what it needs.

There are some very subtle points about all this. Note that we have 3 methods, but only one viewer. The 'index' method actually HTTP redirects to the 'show_books' method. This contains a form that submits to the 'add_book' method, but this has no viewer either: if the submit succeeds, it HTTP redirects to 'show_books', otherwise it *renders* the 'show_books' viewer, but retains its own URL.

About the validation magic, what happens if that if the 'save' call in the 'add_book' method fails, we render the same HTML page we've been rendering all the time, but two things are now different.

In our view there is a call to “error_messages_for 'book'”, which takes the error messages stored in @book, on which we called the '.save' method. Additionally, the 'form_for' handler has access to @book as well, and highlights fields with errors automatically.

If you are actually running this code, you'll notice the output is very ugly. See the section called “Making things pretty” for how to solve this.

Books with authors

One of the major strenghts of the Models in RoR, based on ActiveRecord, is the support for relations among objects of these Models.

In the example above, we stored the author of a book as a simple string, but in the real world, people tend to link books to authors, which have their own table, and which contains more attributes than just the name, if only to keep the spelling straight.

Additionally, this allows us to do things like 'list all books of an author'.

Let's write the Book and Author models:

Table 4. Book and Author model

app/models/book.rbapp/models/author.rb
class Book < ActiveRecord::Base
        validates_presence_of :title
        validates_numericality_of :pages
        validates_presence_of :author
        belongs_to :author
end

		
class Author < ActiveRecord::Base
        validates_presence_of :name
        has_many :books
end
		

This simplistic model expresses that each book belongs to one author, but each author can have many books. The DB scripts that go along with these models:

Table 5. Book and Author model

db/migrate/001_create_books.rbdb/migrate/002_create_authors.rb
class CreateBooks < ActiveRecord::Migration
  def self.up
    create_table :books do |t|
        t.column :title, :string
        t.column :author_id, :integer, :null => false
        t.column :pages, :decimal
    end
  end

  def self.down
    drop_table :books
  end
end

		
class CreateAuthors < ActiveRecord::Migration
  def self.up
    create_table :authors do |t|
        t.column :name, :string
    end
  end

  def self.down
    drop_table :authors
  end
end
		

Note how we added an 'author_id' to the Books migration, which is used to link Books with Authors. Let's head over to the script/console to see how things work:

>> Author.create :name => 'bert hubert'
=> #<Author:0xb753dd44 @attributes={"name"=>"bert hubert", "id"=>4}, @new_record_before_save=true, @new_record=false, @errors=#<ActiveRecord::Errors:0xb753d1dc @errors={}, @base=#<Author:0xb753dd44 ...>>>
>> Author.create :name => 'David Heinemeier Hansson'
=> #<Author:0xb7537408 @attributes={"name"=>"David Heinemeier Hansson", "id"=>5}, @new_record_before_save=true, @new_record=false, @errors=#<ActiveRecord::Errors:0xb75368a0 @errors={}, @base=#<Author:0xb7537408 ...>>>
>> Author.count
=> 2
      

Ok, we've created two Authors, as verified by the last '.count'. Now let's instantiate some books, and try to save them:

<< book = Book.new :title => 'Ruby on Rails The Hard Way', :pages => 17
=> #<Book:0xb7504670 @attributes={"title"=>"Ruby on Rails The Hard Way", "pages"=>17, "author_id"=>nil}, @new_record=true<
<< book.save
=> false
<< book.author = Author.find_first :name => 'bert hubert'
=> #<Author:0xb74c676c @attributes={"name"=>"bert hubert", "id"=>"4"}<
<< book.save
=> true
      

The first save fails, because no author has been set. So we lookup a guy called 'bert hubert' and make him the author, and try again, and this works.

Now the other way around, let's list all books by an author:

<< author = Author.find_first :name => 'bert hubert'
=> #<Author:0xb74b1a24 @attributes={"name"=>"bert hubert", "id"=>"4"}>
<< author.books
=> <books not loaded yet>
<< author.books.collect
=> [#<Book:0xb74ac95c @attributes={"title"=>"Ruby on Rails The Hard Way", "id"=>"5", "pages"=>"17", "author_id"=>"4"}>]
<< author.books
=> [#<Book:0xb74ac95c @attributes={"title"=>"Ruby on Rails The Hard Way", "id"=>"5", "pages"=>"17", "author_id"=>"4"}>]
      

Note how we had to 'collect' the books belonging to this author. This makes sense as we won't be needing to access these books all the time.

The other way around (for the 'belongs_to' association) however is automatic:

>> book = Book.find_first
=> #<Book:0xb7554558 @attributes={"title"=>"Ruby on Rails The Hard Way", "id"=>"5", "pages"=>"17", "author_id"=>"4"}>
>> book.author
=> #<Author:0xb7536724 @attributes={"name"=>"bert hubert", "id"=>"4"}>
>> book
=> #<Book:0xb7554558 @author=#<Author:0xb7536724 @attributes={"name"=>"bert hubert", "id"=>"4"}>, @attributes={"title"=>"Ruby on Rails The Hard Way", "id"=>"5", "pages"=>"17", "author_id"=>"4"}>

Updating the view

Our models have changed, so we need to update the view, but we can keep our controller identical. The view lists all books, and allows us to enter new ones.

The relevant changes are that we can't just print the 'author' of a 'book' anymore, we need to print the 'author.name'. Additionally, when creating a new book, we can't just pass the name of an author, we need a drop-down box with authors, and pass the id.

The new view:

<%= error_messages_for :book %>
<table border=1>
        <tr><td>Title</td><td>Author</td><td>Pages</td></tr>
        <% for item in Book.find(:all) %>
        <tr>      <td><%= h(item.title) %></td>
                <td> <%= h(item.author.name) %> </td>
                <td> <%= h(item.pages) %> </td> </tr>
        <% end %>

<% form_for :book, :url => { :action => :add_book} do |form| %>
        <tr>
                <td>
                        <%= form.text_field :title, :size => 40 %>
                </td>
                <td>
                <%=   form.select( :author_id, Author.find(:all).collect {|p| [ p.name, p.id ] }) %>
                </td>
                <td>
                        <%= form.text_field :pages, :size => 4 %>
                </td>
        </tr>
</table>
<%= submit_tag "Add book" , :class => "submit" %>
<% end %>
      

Note that we only had to change two lines to make this all happen! The 'form.select' line is most complex, but basically says we want to list ':all' authors by name, but use their id for the form action.

RESTful” development

This section was rather hard to write, or at least to research. Quite a lot has been said about 'REST', which is short for “Representational State Transfer”, but like much of Rails, it is hard to see the meat through the hype.

[Warning]Warning

This section deviates a lot from what is usually written about REST, and I may be very wrong here. I make no apologies for this - it is exceedingly hard to find decent documentation that is not outshone by the hype. If I get it wrong, please email me.

We'll dispense with the usual gushing comments about the otherwise unused HTML verbs (like 'PUT', and 'DELETE'), as well as with the benefits of 'format_for', and get right down to why REST is cool

In our previous sections, we made Models, Views and Controllers, and in this respect, REST is no different. Previously, only models were interconnected using relations such as 'belongs_to' and 'has_many', but controllers were separate entities, often acting on many models simultaneously.

The REST paradigm extends the interconnectivity to controllers as well, which as a side effect implies that most models will be acompanied by their own controller (but may have other controllers too).

What does this mean in real life? If we compare our 'books with authors' example above, we'll have a 'book controller' and an 'author controller', where we previously had a 'manager_controller' that managed both books and authors.

In practice this means that our 'book controller' governs the listing, showing and editing of books, and associating them with authors, while the author controller is concerned solely with maintaining authors.

.. to be completed

How does it all work

As stated previously, in the RESTful world, not only do models have relations, so do controllers. In practice, this is reflected in the so called 'routing' configuration, as well as in the actual code of the RESTful controllers.

What about POST, PUT, DELETE etc and format_for?

TBD

Making things pretty

So far we've been outputting complete HTML pages in each view. This is a lot of typing, and inflexible too. So RoR offers another layer of indirection, the 'Layout'. A layout is tied to a Controller, and forms the 'parent' of all views below it.

The layout for a Controller called 'manager' lives in app/views/layouts/manager.rhtml, and is the place to put a basic HTML page, minus the Viewer generated content. This content is inserted by this piece of Ruby: <%= yield :layout %> . app/views/application.rhtml is the global equivalent.

Within this layout, it makes sense to include a cascading style sheet, like this: <%= stylesheet_link_tag "books", :media => "all" %>.

This line expects to find a CSS at public/stylesheets/books.css. The RoR framework outputs a lot of CSS classes, all you have to do is dress these up in the CSS.