
Ruby on Rails is a popular and powerful open source web framework
for rapidly creating high-quality web applications to help you keep up with the
speed of the Web. Rails is thriving on Mac OS X, and Leopard comes pre-installed
with Ruby, Rails, Mongrel, Capistrano, Subversion, and other tools that help to
streamline the development and deployment of Rails applications. In addition,
the Organizer feature of XCode 3.0 keeps your development workflow
efficient.
This article gives you a full tour of Ruby on Rails 2.0 on
Leopard—starting with building a web application using the latest Rails
features with Xcode 3.0, and finishing with deploying the application to a
production server running Leopard Server. Along the way we'll explore unique
features and benefits that Leopard brings to the party. In the end you'll be
better equipped to consider the advantages of powering your web application
with Rails on Leopard.
This is the first in a series of three articles:
- This article on Development, where you learn to build a basic RESTful Rails application using Xcode 3.0;
- Customization, where we discuss working with views and web forms, adding AJAX support, and supporting an iPhone interface;
- Deployment, where we set up version control, write a Capistrano recipe, and deploy on Leopard Server.
Together they will give you a great start in working with Rails on Mac OS X Leopard.
Ruby, Rails, and Leopard
In Leopard and Leopard Server, Ruby and Rails are pre-installed along
with a bounty of other useful RubyGems. This means we can hit the
ground running when it comes to developing a Rails application and keep
up the pace when deploying new releases of our application to Leopard
Server.
Out of the box, you get Ruby version 1.8.6 and Rails version 1.2.6, the latest
stable releases at the time Leopard shipped. Ruby releases are few and far
between (it's still at 1.8.6), but Rails has frequent new releases. In fact,
the application we'll build requires Rails 2.0.2. The good news is it's easy
to upgrade Rails and RubyGems. Make sure your system is up to date now by
running these commands:
sudo gem update --system
sudo gem install rails
sudo gem update rake
sudo gem update sqlite3-ruby
The first command updates the RubyGems system itself, which is required by the
latest version of Rails. The second command updates Rails-specific gems and
installs any new components (specifically Active Resource). The third command
updates Rake, the make-like build tool used by Rails. Finally, the
last command updates the Ruby bindings for the SQLite3 database. Running all
these commands may take a while. When it's finished, you can list all the
installed gems by typing
gem list
You can view the documentation for all the installed gems by running
gem server
This command starts a web server on port 8808 by default. To access the documentation, point Safari to
http://localhost:8808/.
You'll see a list of the gems you have installed with a link to their RDoc documentation.
If you've used Ruby before, you may be interested to know where the Ruby
gems live in Leopard. The answer is in two places:
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/gems/1.8
/Library/Ruby/Gems/1.8
The /System repository is where all the pre-installed gems live,
while the /Library repository is empty by default. When you install
new gems, they'll end up in the /Library repository. Thankfully, it's
all transparent.
Creating a New Rails Application
Note: This tutorial uses Rails version 2.0.2. With future Rails updates, some of the functionality described in this article may change.
Let's say, for example, that we have a club that organizes fund-raising events. When we
put on an event we incur expenses from vendors who provide goods and services.
Our club operates on a fairly tight budget, so we need to make sure expenses
are in line with the budget for each event. The old paper and pencil system
broke down last month and we've decided to replace it with an online expense
tracking application that everyone can share.
First we need to create the application. All Rails applications have a
consistent directory structure so that Rails can find stuff without needing to
be told where to look. In a directory of your choosing (e.g., your home directory), create a skeleton application directory structure and a set of files
for our expenses application by typing
rails expenses
That command generates a number of directories and files in an
expenses directory. We'll be editing various files as we go, and to
streamline your development workflow you'll want quick access to frequently
used files. Xcode 3.0 in Leopard has an Organizer feature that makes it easy
to navigate around Rails projects. You don't even need to create a new Xcode
project.
Follow these steps to open the Rails application in the Organizer:
-
Launch Xcode and choose "Window > Organizer". An empty
Organizer window appears.
-
Drag your expenses directory from a Finder window into the
Organizer window.
-
Click the disclosure triangle next to the top-level expenses directory to display its contents.
-
Drill down into the app/controllers directory and select
the application.rb file.
-
Display the text editor pane by clicking the rectangle icon in the
bottom-left corner of the Organizer.
In the Organizer window, shown in Figure 1, you should see the files and
directories that the rails command created. You'll notice in the
Organizer that you have familiar Xcode editing features such as syntax
coloring, code folding, line numbers, method shortcuts, and so on.
Figure 1:
Xcode 3.0 Organizer showing the Rails application directory structure
At this point, we could go ahead and fire up our expenses application from a Terminal command line using the script/server command. However,
in addition to giving you a central place to edit files, the Xcode Organizer also lets you assign actions to folders. So rather than running the Rails application in a separate Terminal window, we'll assign an action to run it straight from the Organizer.
Follow these steps to create a new Organizer Action to run the Rails application:
-
Select the top-level expenses folder.
-
Click and hold the Run toolbar item until a pop-up menu appears.
Choose "Edit Actions..." and you should see the Action Editor
as shown in Figure 2.
Figure 2:
Xcode 3.0 Organizer Action Editor
The list on the left side of the Action Editor displays actions
associated with the Run toolbar item, along with their with optional
shortcut keys. On the right side is a drop-down directory specifier and
a script editor.
-
Add a new Shell Script Action using the add button (+) in the
bottom-left corner of the Organizer.
Name the action "start server" by double-clicking on the
default name "Shell Script".
-
Set the directory to
"Top Level Organizer Item". This indicates that the action's
command should be run from the top-level folder (expenses),
regardless of the folder that's currently selected when the action is
run.
-
Change the command to "script/server" and clear out the
arguments.
The Action Editor should now look like the one shown in Figure 3.
Figure 3:
Adding a new Organizer Action
-
Click "OK" to save the new action.
-
Back in the Organizer window, select the expenses folder, then
click and hold the Run toolbar item. When the pop-up menu appears,
choose the "start server" action. (Or just click the Run
button and it'll run that action since there's only one assigned
action.)
A Debugger window opens and starts the server with output similiar
to the output shown in Figure 4.
Figure 4:
Running an Organizer Action
-
Now point Safari at http://localhost:3000. You should see a
web page welcoming you aboard Rails.
So far, we've stood up a brand new application and we're organized, but it
doesn't help our fund-raising campaign. Let's fix that.
Jump-Starting the Application
Now we're ready to put some meat on the bones of the skeleton directory
structure. For starters, we need a web interface for creating some events in
the database. An event is simply a name and a budgeted amount. The fastest way
to put up a web interface backed by a database is by using what Rails calls
scaffolding. Scaffolding is the initial supporting code that handles
the basic create, read, update, and delete (CRUD) operations of any
database-backed web application.
Open a Terminal window, then change directory to the expenses
directory and create the event scaffolding by typing
script/generate scaffold event name:string budget:decimal
This single command generates all the Ruby code we need to start administering
events:
-
Model: An Event model class in the
app/models/event.rb file
-
View: View template files in the app/views/events
directory for listing, showing, creating, and editing events
-
Controller: An EventsController class in the
app/controllers/events_controller.rb file
-
Database Migration: A CreateEvents class in the
db/migrate/001_create_events.rb file that when run
creates an events database table with three columns:
id, name, and budget
We'll look at these files in more detail shortly. All we need to do now is
apply the database migration to create the events database table so
we can start creating events. Rails applications use Rake (Ruby's equivalent
of Make) to automate recurring tasks such as applying migrations. We could run
rake db:migrate from a Terminal command line, but again the Organizer
makes this easier. All the Rake tasks are already available as pre-configured
actions.
Click and hold the Action toolbar item, then choose the "db:migrate"
action (the first one). A window opens showing the results of applying the
migration, as shown in Figure 5.
Figure 5:
Applying the events database migration
Let's see how close scaffolding gets us to a web interface for administering
events. Point Safari at http://localhost:3000/events. You
should see a web page that lets you create new events.
Go ahead: create an event and the new event will show up in the event listing.
From there you can show, edit, or delete the event. Figure 6 shows example
events for our fund-raising campaign.
Figure 6:
Using scaffolding to administer events
That takes care of event management. Now let's repeat the cycle to put up a
web interface for administering vendors. Generate the vendor scaffolding by
typing
script/generate scaffold vendor name:string email:string
This command generates all the supporting code for a vendor web interface,
including a database migration in the
db/migrate/002_create_vendors.rb file.
Apply the new migration to update the database schema to include the
vendors table by choosing the "db:migrate" action from the
Action toolbar item. The results are shown in Figure 7.
Figure 7:
Applying the vendors database migration
Then point Safari at http://localhost:3000/vendors. You
should see a web page that lets you create new vendors.
To recap: In a very short amount of time, and with no explicit configuration,
we've created a database-backed web application to CRUD (the verb form) events
and vendors. It won't win any web design awards, but it's functional
and gives us a jump-start on our application.
How It Works
Looking at the code generated by the script/generate scaffold command is a great way to learn how Rails
applications work. After all, the templates used by the generator were written
by the Rails core team. And as we'll see a bit later, scaffold-generated code
serves merely as an exemple that should be tweaked as necessary for your
application.
Let's follow a request through the request/response cycle, looking inside the
scaffold-generated code as we go to see how all the components work together.
Figure 8 shows the MVC components in play for events, and their file
equivalents.
Figure 8:
MVC components and their files
RESTful Conventions
The speed at which we were able to create a fully-functioning web application
can be credited in large part to conventions. Rails relies heavily on
conventions to eliminate most configuration, thus making web developers a lot
happier.
Take, for example, the "Show" hyperlink for an event. In the
app/views/events/index.html.erb template, the hyperlink is generated
using
<%= link_to 'Show', event %>
That line of code generates the following HTML, for example:
<a href="/events/1">Show</a>
When this hyperlink is clicked in Safari, it ends up executing Ruby code
inside the Rails application. But how does Rails know how to route an incoming
URL to the appropriate code that handles the request? The answer: conventions.
It turns out that the HTTP protocol already has conventions for interacting
with resources on the web via a simple set of verbs: GET,
POST, PUT, DELETE, etc. With every HTTP request
comes one of these verbs. The URL identifies the resource (the noun,
if you will). For example, clicking the http://localhost:3000/events/1 link
issues a GET request for the event living at that web address.
This resource-oriented approach to web design is an example of the REST
architectural style, and Rails has whole-heartedly embraced it. Now, REST is a
relatively complex (and academic) topic that's beyond the scope of this
article. Thankfully you don't have to be a REST expert to use Rails. You just
need to tell Rails which resources should be accessible via the RESTful
conventions. Rails then takes care of routing incoming HTTP verbs and URLs to
those resources.
When we ran the scaffold generator for events, for example, Rails
assumed that events are resources we want to expose via RESTful
conventions. The same is true for vendors. So if you look in the
config/routes.rb file you'll see that the following lines were
automatically added:
map.resources :events
map.resources :vendors
Those two lines of code dynamically add routes for accessing our events
and vendors according to the RESTful conventions. To see all the routes,
choose the "routes" action from the Action toolbar item.
Let's look at just a few of the routes in the list you'll see to get a glimpse
of what's going on behind the scenes:
events GET /events {:controller => "events", :action => "index"}
POST /events {:controller => "events", :action => "create"}
event GET /events/:id {:controller => "events", :action => "show"}
PUT /events/:id {:controller => "events", :action => "update"}
In a nutshell, the map.resources line generates routes into our
application using both the incoming HTTP verb and URL. For example, to list
all the events we'd send in the URL /events. The exact same incoming
URL is used to create a new event. The only difference is the HTTP verb that is
used: GET is a read-only operation that lists the events and POST is a write
operation that creates a new event. To show or update an existing event, the
URL needs to include the event id (primary key), for example
/events/1. Again, the URL is the same, but the HTTP verb is
different: GET fetches the event and PUT updates the event.
Controllers
Now let's follow the request back into scaffold-generated MVC code. When Rails
receives a GET request for the /events/1 resource, for
example, it invokes the show method (called an action) of the
EventsController. That action is found in the
app/controllers/events_controller.rb file.
class EventsController < ApplicationController
def show
@event = Event.find(params[:id])
respond_to do |format|
format.html # renders show.html.erb
format.xml { render :xml => @event }
end
end
# plus other action methods for listing, creating,
# updating, and deleting events
end
The show action uses the event id supplied in params[:id] (1, in this case) to
find a matching event in the events database table. It then populates
an Event model object with the values corresponding to that database
table row. The resulting Event model object is assigned to the
@event instance variable. (Ruby identifies instance variables as
variables starting with an @ symbol.)
Models
The Event model class itself lives in the
app/models/event.rb file.
class Event < ActiveRecord::Base
end
At first glance, this class appears to do nothing, but in fact it already has
a lot of functionality inherited through its ActiveRecord::Base
parent class. ActiveRecord is a component of Rails that, among other
things, uses reflection and naming conventions to transparently map relational
database tables into models. By convention, the Event model
encapsulates access to the events table in our database. An instance
of an Event model represents a row in the events
table, with each table column mapping to an attribute of the Event
instance.
For example, without any additional configuration or code, inside of
the show action we could get the name of the event using:
@event.name
Database Migrations
Now you may be wondering which database Rails is using and how it got
connected. When we created the application, a config/database.yml
file was generated. In that file you'll notice that Rails has pre-configured
three database connections—development, test, and
production—that correspond to three runtime environments.
We're doing development now (the default environment), so have a look at that
particular section of the config/database.yml file.
development:
adapter: sqlite3
database: db/development.sqlite3
timeout: 5000
Rails configures a SQLite3 database by default, which is quite handy given
that SQLite3 is already installed on Leopard. When it comes time to deploy this
application, we'll switch over to a MySQL database running on Leopard Server.
When we ran the first database migration to create the events table,
the db/development.sqlite3 database was automatically created. Here's
what the db/migrate/001_create_events.rb file looks like:
class CreateEvents < ActiveRecord::Migration
def self.up
create_table :events do |t|
t.string :name
t.decimal :budget
t.timestamps
end
end
def self.down
drop_table :events
end
end
Migrations give us a way to evolve our database schema and data over time in a
repeatable, automated way. Each migration defines two class-level methods:
up and down. The up method moves the migration
"forward", in this case by creating the events table and
its columns. Note that create_table will implicitly create a primary
key column called id by default.
The down method rolls the migration "backward", in this
case by dropping the events table. That is, migrations are
reversible. For example, if you wanted to drop the events table,
you'd type 'rake db:rollback'.
Views
Back in the show action, we've fetched the event from the database
and now it's time to show it. The respond_to block decides how to
render the event depending on the format requested by the client. Since we're
using Safari as the client (and we haven't explicitely specified a format),
the format will be HTML. So in this case, the show action renders the
template found in the app/views/events/show.html.erb file.
<p>
<b>Name:</b>
<%= h @event.name %>
</p>
<p>
<b>Budget:</b>
<%= h @event.budget %>
</p>
<%= link_to 'Edit', edit_event_path(@event) %> |
<%= link_to 'Back', events_path %>
Rails view templates are a mix of HTML and Ruby code. Any expression between
<%= and %> is evaluated as Ruby code and the result is
substituted back into the response. Notice that the show template has
access to the @event instance variable that was created in the
show action of the EventsController. Using the
@event variable, the show template simply outputs the value
of each event attribute with its associated column name.
The template then generates hyperlinks to the edit and index
actions using the link_to helper. The edit_event_path and
events_path methods were generated by the map.resources
:events declaration we saw previously. They create URLs according to the
RESTful conventions that route back into the application.
At this point we've followed one HTTP request through the scaffold-generated
code to generate an HTML page as the response. Now let's start adding some
code of our own.
Validating Models
You may have noticed that the scaffold-generated web form used to create and
edit events will let you enter anything in the fields, and even let you leave
required fields blank. That's no good—we only want valid data in our database.
Maintaining the consistency of the application's data is a model's job. Rails
includes a rich set of built-in model validations and it's easy to write your
own, as well. So let's add some model validations to ensure an event has a
name and a valid budget amount.
Update the app/models/event.rb file as follows:
class Event < ActiveRecord::Base
validates_presence_of :name
validates_numericality_of :budget, :greater_than => 0.0
end
Now try to create an event with a blank name or invalid budget amount. You
should see something similar to the web form shown in Figure 9.
Figure 9:
Model validations at work
Linking Models Together
Administering events and vendors is a good start, but we're still missing the
ability to track expenditures. An expense is simply an amount charged by a
vendor for an event. In database terms that means an expense needs to have
foreign keys pointing to its event and vendor. In other words, an expense
joins together an event and a vendor. The database schema we're aiming for is
shown in Figure 10.
Figure 10:
Many-to-many association database schema
Just like event and vendor, we want an expense to be a resource. The only
difference is we don't need a full scaffold-generated web interface for
expenses. Instead, we're going to record expenses using our existing events
web interface. However, we still need a migration, a model, and a controller
for expenses.
Generate a new expense resource by typing:
script/generate resource expense event_id:integer vendor_id:integer amount:decimal
Naming is important here. Rails assumes that the event_id foreign key
column references the id column of the events table, and
likewise vendor_id references the id column of the
vendors table. It's yet another example of how Rails uses naming
conventions to keep external configuration at a minimum. (You can always
override the default behavior.)
Then apply the migration that was generated in the
db/migrate/003_create_expenses.rb file to create the
expenses database table by choosing the "db:migrate" action
from the Action toolbar item. The results are shown in Figure 11:
Figure 11:
Applying the expenses database migration
At this point we have an expenses database table with foreign key
references to the events and vendors tables. That's one side
of the coin. Rails still doesn't know exactly how to use those references in
terms of model associations.
Specifically, we want the Expense model to have a many-to-one
relationship with the Event model and a many-to-one relationship with
the Vendor model. And because an expense knows about both its event
and its vendor, the Expense model will implicitly represent a
many-to-many relationship between events and vendors.
Declare the associations in the models as follows:
class Expense < ActiveRecord::Base
belongs_to :event
belongs_to :vendor
end
class Event < ActiveRecord::Base
validates_presence_of :name
validates_numericality_of :budget, :greater_than => 0.0
has_many :expenses
has_many :vendors, :through => :expenses
end
class Vendor < ActiveRecord::Base
has_many :expenses
has_many :events, :through => :expenses
end
Expense now has a many-to-one relationship with both Event
and Vendor. We've also created an indirect relationship between
Event and Vendor going "through" the Expense join
model. The declarations read quite nicely: An expense belongs_to an
event and an event has_many expenses. We can declare the model
associations in a concise way and through the power of Ruby those
declarations dynamically add methods to the enclosing class for managing the
association.
Accessing Models Through the Back Door
Adding just a couple lines of code to the models has given us a bunch of new
functionality. Before we start using it in our web application, let's
experiment a bit with linking our models together. Type
script/console
That command loads our Rails application into an interactive environment, then
prompts you (shown below as the >> prompt) to enter Ruby code.
It shows the value of each expression (shown below after =>)
after you press Return.
Start by creating two new vendors in the database:
>> vendor1 = Vendor.create(:name => 'Parties R Us')
=> #<Vendor id: 1, name: "Parties R Us", ...>
>> vendor2 = Vendor.create(:name => 'Fire Department')
=> #<Vendor id: 2, name: "Fire Department", ...>
Next find an event you created earlier in the web interface:
>> event = Event.find_by_name('Chili Cookoff')
=> #<Event id: 1, name: "Chili Cookoff", ...>
Then create two expenses that link the event to each vendor:
>> event.expenses.create(:vendor => vendor1, :amount => 75.00)
=> #<Expense id: 1, event_id: 1, vendor_id: 1, ...>
>> event.expenses.create(:vendor => vendor2, :amount => 25.00)
=> #<Expense id: 2, event_id: 1, vendor_id: 2, ...>
Now you can access all expenses that directly belong to the event and
all vendors that indirectly belong to the event through the expenses:
>> event.expenses
=> [#<Expense id: 1, ...>, #<Expense id: 2, ...>]
>> event.vendors
=> [#<Vendor id: 1, ...>, #<Vendor id: 2, ...>]
You can also calculate the total expenses for the event:
>> event.expenses.sum(:amount)
=> #<BigDecimal:1a04960,'0.1E3',4(8)>
Try navigating through the same associations, this time starting with a Vendor object. Then use quit to exit the session.
Adding Business Logic
Now that we can record expenses for a specific event, we can calculate the
total expenses to see if we're within budget for the event. Models not only
wrap database tables, they also encapsulate business logic such as this. So
add the following method to the Event model:
def total_expenses
expenses.sum(:amount) || BigDecimal("0.0")
end
This instance method uses the expenses relationship so that only those expenses belonging to a specific event are accumulated. We used similar code previously in script/console. Bottling those details up into
a well-named method of the Event model helps keep our code clean and reusable. For example, we can also use the total_expenses method to
write a new budget_exceeded? method to tell us whether an event
is within budget. Add the following method to the Event model:
def budget_exceeded?
total_expenses > budget
end
Writing Tests
Whenever we add business logic, trivial as it may be, we're wise to test it.
Rails makes doing the right things easy. One of those things is testing. When
we generated the event scaffolding, Rails went ahead and created a unit
test for the Event model and a functional test for the
EventsController. All we need to do is write a new unit test to cover
the business logic we added to the Event model.
Add the following test method to the test/unit/event_test.rb file:
def test_budget
event = Event.new(:name => 'Test Event', :budget => 30.00)
event.expenses.build(:amount => 10.00)
event.expenses.build(:amount => 20.50)
assert event.save
assert_equal BigDecimal("30.50"), event.total_expenses
assert event.budget_exceeded?
end
The test uses the build method to create two expenses (in memory) and
add them to the event's expenses association. Then the test saves the
event (and its expenses) to the test database and asserts that the save
operation returned true. With the event tucked away in the database,
the test then checks that the event's total expenses is the sum of the
expenses and that the expenses exceed the budget.
Run all the unit tests by choosing the "test:units" action from the
Action toolbar item. All the tests should pass with the following output:
....
Finished in 0.1223 seconds.
4 tests, 6 assertions, 0 failures, 0 errors
Remember that Rails has three default runtime environments. When you run
tests, Rails switches to the test environment. Among other things,
that means the database configured in the test section of the
config/database.yml file will be used. In our case it's a SQLite3
database in the db/test.sqlite3 file. The upshot is tests can muck
with the database without affecting our development (or production) data.
In addition to unit tests, functional tests let you easily generate simulated
HTTP requests against a controller, then assert that the controller works as
you'd expect. You can also write integration tests to assert
expectations across multiple controllers. To run all the unit, functional, and
integration tests in one fell swoop, simply run rake. (As it stands,
the functional tests fail because we've added validations to the
Event model.)
In the second article in this series, Customizing Rails Applications on Mac OS X Leopard, we build
on this basic Rails application, including customizing views, working
with web forms, adding AJAX support, and supporting an iPhone interface
on Mac OS X Leopard.
Updated: 2008-03-28
|