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:
Together they will give you a great start in working with Rails on Mac OS X 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.
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.
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.
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
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.
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.)
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
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
'.
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.
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
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.
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.
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
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-09-29