User:WillWare/Learning Ruby on Rails
Getting started
[edit]I've moved notes about the Ruby language itself to their own separate page.
Good books:
- The Ruby on Rails Wikibook[1]
- Agile Web Development with Rails[2] by Thomas Heinemeier Hansson, published by Pragmatic Bookshelf
- Rails Pocket Reference[3] by Eric Berry, published by O'Reilly
Rails tutorials:
- http://betterexplained.com/articles/starting-ruby-on-rails-what-i-wish-i-knew/ -- I respect this guy's opinion and he recommends getting set up with the following.
- Agile Web Development with Rails, better than the online tutorials[4]
- InstantRails[5]: A .zip file containing Ruby, Apache, MySQL and PHP (for PhpMyAdmin), packaged and ready to go.
- Aptana/RadRails[6] (like Eclipse) or Ruby In Steel[7] (like Visual Studio) for editing code.
- Subversion[8] and/or TortoiseSVN[9] for source control. Personally, I prefer Mercurial.
- Browse popular ruby on rails links at del.icio.us/popular/rubyonrails, Rails documentation[10] and Ruby syntax and examples[11]. I would have provided a link for the del.icio.us site except Wikipedia considers them a spam source, and prevents linking to them.
- http://guides.rails.info/getting_started.html
- http://railscasts.com/
Install Ruby and Rails on Ubuntu, and create a Rails application called "first". Look at all the files and directories. The app-specific stuff is in first/app.
$ sudo aptitude install irb rails
$ mkdir ~/playWithRails
$ cd ~/playWithRails
$ rails first
Now we can turn on the server for this application.
$ cd first
$ ruby script/server
Go to your web browser and look at http://localhost:3000/ and behold, Rails is working. Hit control-C on the server process to get your prompt back.
A controller that does something
[edit]Rails follows the model-view-controller pattern. Let's set up an app.
$ ruby script/generate controller Hello
This has produced a controller, but it doesn't do anything yet. It's simply a class that inherits from ApplicationController but is otherwise empty.
class HelloController < ApplicationController
end
Remember how amazingly ugly CGI scripts were? Rails offers the same functionality without making you throw up a little bit in the back of your mouth.
In Rails parlance, an action is something the controller can do. Controller actions correspond to the "action" of an HTML form, the thing to be done when you the Submit button. To go with each action, there is a view that tells what HTML to send back as a response.
So let's do some HTML form stuff. In this example, "there", "handleCheckbox", and "handleMultipleSelect" are controller actions. The at-sign (@) in front of "data" indicates an instance variable of this class.
class HelloController < ApplicationController
def there
end
def handleCheckbox
@data = params[:check1]
end
def handleMultipleSelect
@data = params[:multi1]
end
end
Here are some views
[edit]Put this in first/app/views/hello/there.rhtml
<html>
<head>
<title>Fun with HTML forms</title>
</head>
<body>
<h1>Now witness the firepower of this fully armed
and operational battle station!</h1>
<form action="/hello/handleCheckbox">
Try the checkbox: <input type="checkbox" name="check1"><br>
<input type="submit"></form><p>
<form action="/hello/handleMultipleSelect">
Select all that apply, using shift and control keys:<br>
<select multiple size="4" name="multi1[]">
<option value="stuck up">stuck up
<option value="half-witted">half-witted
<option value="scruffy-looking">scruffy-looking
<option value="Nerf herder">Nerf herder
</select><br>
<input type="submit"></form><p>
</body>
</html>
Put this in first/app/views/hello/handleCheckbox.rhtml. Here we are using "<%" and "%>" to embed bits of Ruby code into the HTML. You may have seen things like this done in PHP or JavaServer Pages or Active Server Pages. This has an acronym in Rails parlance: "ERb", for "embedded Ruby".
<html>
<head>
<title>Fun with checkboxes</title>
</head>
<body>
<h1>Results of your experience with the checkbox</h1>
<% if @data %>
I see you have checked the checkbox.
Your skills are complete.
Indeed you are powerful as the Emperor has foreseen.
<% else %>
Incomplete was your training.
Not ready for the checkbox were you.
<% end %>
</body>
</html>
Put this in first/app/views/hello/handleMultipleSelect.rhtml. Here we're using "<%=" instead of "<%", meaning that the value of the Ruby expression should appear as part of the HTML rendered in your browser.
<html>
<head>
<title>Fun with multiple selections</title>
</head>
<body>
<h1>Results of your experience with the multiple selection</h1>
Why, you
<% for data in @data %>
<%= data %>
<% end %>
</body>
</html>
Scaffolding and database-backed apps
[edit]Accessing a database
[edit]Ruby works with databases like MySQL or Oracle using ActiveRecords which implement object-relational mapping, also used in Django and Hibernate. SQL tables are represented as classes, rows are represented as instances of those classes, and columns are represented as properties of the class.
By default, Rails uses SQLite, but here I'll use MySQL because it's more scalable to large datasets. Once you've got MySQL working, it's a short hop to even bigger databases like Oracle, if you suddenly discover that your web app is popular beyond reason. On Ubuntu, you'll first need to provide Ruby with access to MySQL.
$ sudo aptitude install libmysqlclient-dev
$ sudo gem install mysql
Create an application which will use the database. Let's make it a list of our favorite movies.
$ rails movie
$ cd movie
$ vi config/database.yml # see below...
The database.yml file (a YAML file) is used to configure how the database will be accessed.
development: adapter: mysql database: Movie username: root password: **** pool: 5 timeout: 5000
Creating a scaffold
[edit]Next generate a scaffold for a simple database-backed app, create the database in MySQL, and start the application server. The scaffold is a starting point, minimalist in every sense. The objects are POJOs, the HTML presentation is bare, the app logic is a direct viewer/editor of database table contents.
$ ruby script/generate scaffold Movie title:string description:text url:string
$ ruby script/generate scaffold Actor name:string title:string
$ rake db:create:all
$ rake db:migrate
$ ruby script/server
This creates the following table in MySQL.
mysql> use Movie;
Database changed
mysql> describe movies;
+-------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| title | varchar(255) | YES | | NULL | |
| description | text | YES | | NULL | |
| url | varchar(255) | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| updated_at | datetime | YES | | NULL | |
+-------------+--------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
It also creates app/controllers/movies_controller.rb, ready to handle a variety of HTTP requests.
class MoviesController < ApplicationController
# GET /movies
# GET /movies.xml
def index ... end
# GET /movies/1
# GET /movies/1.xml
def show ... end
# GET /movies/new
# GET /movies/new.xml
def new ... end
# GET /movies/1/edit
def edit ... end
# POST /movies
# POST /movies.xml
def create ... end
# PUT /movies/1
# PUT /movies/1.xml
def update ... end
# DELETE /movies/1
# DELETE /movies/1.xml
def destroy ... end
end
Now visit http://localhost:3000/movies in your browser, and you've got a very simple little movie database. For your first movie, enter a classic.
Title: Alien Description: Sci-fi horror Url: http://www.imdb.com/title/tt0078748/
Customizing the app
[edit]The scaffold command is intended only to give you a starting point for your app. It lays out a minimal framework which you can then modify. So feel free to tweak the logic and presentation to suit your needs and preferences. I put a visible border around the cells in the HTML table, and replaced the URL with an HTML link. Notice the "<%=h" prefix used below, where the "h" means that the text will be URL-escaped. We don't want that with the URL.
...
<table border="1">
...
<% for movie in @movies %>
<tr>
<td><%=h movie.title %></td>
<td><%=h movie.description %></td>
<td><a href="<%= movie.url %>">Link</a></td>
...
</tr>
<% end %>
</table>
...
ActiveRecord and Object-Relational Mapping
[edit]Like Django and Hibernate, Rails uses classes to represent database tables, with getters and setters for columns, and class instances represent rows in the table. This is called object-relational mapping or ORM. In Rails, these classes extend a base class called ActiveRecord[12].
Let's look at how ORM works in Rails. The BetterExplained.com website has some good explanatory stuff[13].
We've just finished running the scaffold command, and we look to see what's been done. The file app/models/movie.rb is nearly empty, and probably intended for subsequent customization. The setters and getters for the fields are hiding inside the ActiveRecord base class, but we can override them to do special things if desired.
class Movie < ActiveRecord::Base
end
Next we have db/schema.rb, which (SURPRISE) specifies the database schema. In addition to the fields specified in the scaffold command, we also have "created_at" and "updated_at", and our database schema has been assigned a version number based on a timestamp.
ActiveRecord::Schema.define(:version => 20100228142908) do
create_table "movies", :force => true do |t|
t.string "title"
t.text "description"
t.string "url"
t.datetime "created_at"
t.datetime "updated_at"
end
end
Queries
[edit]If ORM is a way to encapsulate the database, there must be ORM equivalents for common SQL queries. A typical SQL query would request a set of database rows that matched zero or more criteria, like this.
- SELECT * FROM movies WHERE title="Gone with the Wind"
Here is the equivalent thing in Rails. More information[14] is available.
# find first
Person.find(:first) # returns the first object fetched by SELECT * FROM people
Person.find(:first, :conditions => [ "user_name = ?", user_name])
Person.find(:first, :conditions => [ "user_name = :u", { :u => user_name }])
Person.find(:first, :order => "created_on DESC", :offset => 5)
# find last
Person.find(:last) # returns the last object fetched by SELECT * FROM people
Person.find(:last, :conditions => [ "user_name = ?", user_name])
Person.find(:last, :order => "created_on DESC", :offset => 5)
# find all
Person.find(:all) # returns an array of objects for all the rows fetched by SELECT * FROM people
Person.find(:all, :conditions => [ "category IN (?)", categories], :limit => 50)
Person.find(:all, :conditions => { :friends => ["Bob", "Steve", "Fred"] }
Person.find(:all, :offset => 10, :limit => 10)
Person.find(:all, :include => [ :account, :friends ])
Person.find(:all, :group => "category")
Migration
[edit]Migration is the process of making changes to the database schema without trashing all the data you've collected. The version number (20100228142908) helps Rails to keep track of things while migrating. The actions to be performed for a migration of a particular version of a particular SQL table appear in source files like db/migrate/20100228142908_create_movies.rb.
class CreateMovies < ActiveRecord::Migration
def self.up
create_table :movies do |t|
t.string :title
t.text :description
t.string :url
t.timestamps
end
end
def self.down
drop_table :movies
end
end
Rails maintains a table called schema_migrations, notating which migrations have already been done, so it doesn't repeat them.
Connecting Rails to a pre-existing MySQL database
[edit]I've been asked by a customer to connect his already-in-place MySQL database to a Rails app. Looking at the table schemas, I can see two potential problems. The first is that there are plenty of "VARCHAR(N)" fields where N is not 255, the value that Rails assigns by default. Luckily this number can be changed, so we could switch to a movie title of VARCHAR(100) like this.
class CreateMovies < ActiveRecord::Migration
def self.up
create_table :movies do |t|
t.string :title, :limit => 100
t.text :description
t.string :url
t.timestamps
end
end
def self.down
drop_table :movies
end
end
My other concern is that Rails likes to append two timestamps at the end of a table schema, "created_at" and "updated_at", but these aren't present in my customer's database. I can remove the references to the timestamps from db/schema.rb and db/migrate/20100228142908_create_movies.rb, and re-run "rake db:migrate". From what I can tell so far, that's OK.
- <YIKES> I missed one more thing to worry about. Rails has added an "id" integer to each database entry, and I don't see any way to remove it. The relevant table in my customer's code already has an "id" integer field. When I do "rake db:migrate", will all those integer id's be reassigned?
- Probably the best approach would be to back up the existing database to some other form of storage, and then be able to reconstruct it in its exact original (pre-Rails) form if I screw anything up. This could be done by collecting the output from "SELECT * FROM tablename" statements, and then do some Python scripting of those. </YIKES>
URLs and URL Routing
[edit]If we skip all the contents in config/routes.rb, this is what we have immediately after the scaffold command.
ActionController::Routing::Routes.do |map|
map.resources :movies
# Each SQL table gets a "map.resources" line here
map.connect ':controller/:action/:id'
map.connect ':controller/:action/:id.:format'
end
Note that routes can include information about the format we want in the response. If we set up the movie example above and then browse http://localhost:3000/movies.xml, we see this.
<?xml version="1.0" encoding="UTF-8"?>
<movies type="array">
<movie>
<created-at type="datetime">2010-02-28T14:33:40Z</created-at>
<description>Sci-fi horror, this is a great movie, one of the best of that year.</description>
<id type="integer">1</id>
<title>Alien</title>
<updated-at type="datetime">2010-02-28T14:34:02Z</updated-at>
<url>http://www.imdb.com/title/tt0078748/</url>
</movie>
<movie>
<created-at type="datetime">2010-02-28T14:45:02Z</created-at>
<description>Really brainless sci-fi</description>
<id type="integer">2</id>
<title>The Matrix</title>
<updated-at type="datetime">2010-02-28T14:45:02Z</updated-at>
<url>http://www.imdb.com/title/tt0133093/</url>
</movie>
</movies>
This is very useful because it means we can get Rails to serve XMLHttpRequests used in AJAX applications.
Routing examples
[edit]These are taken from the comments that are automatically inserted into config/routes.rb when you run the scaffold command.
The priority is based upon order of creation: first created -> highest priority. You'll want to install the default routes as the lowest priority. Default routes make all actions in every controller accessible via GET requests. You should consider removing the them or commenting them out if you're using named routes and resources.
Sample of regular route:
map.connect 'products/:id', :controller => 'catalog', :action => 'view'
You can assign values other than :controller and :action.
Sample of named route:
map.purchase 'products/:id/purchase', :controller => 'catalog', :action => 'purchase'
This route can be invoked with purchase_url(:id => product.id)
Sample resource route (maps HTTP verbs to controller actions automatically):
map.resources :products
Sample resource route with options:
map.resources :products, :member => { :short => :get, :toggle => :post }, :collection => { :sold => :get }
Sample resource route with sub-resources:
map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
Sample resource route with more complex sub-resources
map.resources :products do |products|
products.resources :comments
products.resources :sales, :collection => { :recent => :get }
end
Sample resource route within a namespace:
map.namespace :admin do |admin|
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
admin.resources :products
end
You can have the root of your site routed with map.root -- just remember to delete public/index.html.
map.root :controller => "welcome"
You can see how all your routes lay out with "rake routes". When applied to the movie database app, we get this.
$ rake routes
(in /home/wware/playWithRails/movie)
movies GET /movies {:controller=>"movies", :action=>"index"}
formatted_movies GET /movies.:format {:controller=>"movies", :action=>"index"}
POST /movies {:controller=>"movies", :action=>"create"}
POST /movies.:format {:controller=>"movies", :action=>"create"}
new_movie GET /movies/new {:controller=>"movies", :action=>"new"}
formatted_new_movie GET /movies/new.:format {:controller=>"movies", :action=>"new"}
edit_movie GET /movies/:id/edit {:controller=>"movies", :action=>"edit"}
formatted_edit_movie GET /movies/:id/edit.:format {:controller=>"movies", :action=>"edit"}
movie GET /movies/:id {:controller=>"movies", :action=>"show"}
formatted_movie GET /movies/:id.:format {:controller=>"movies", :action=>"show"}
PUT /movies/:id {:controller=>"movies", :action=>"update"}
PUT /movies/:id.:format {:controller=>"movies", :action=>"update"}
DELETE /movies/:id {:controller=>"movies", :action=>"destroy"}
DELETE /movies/:id.:format {:controller=>"movies", :action=>"destroy"}
/:controller/:action/:id
/:controller/:action/:id.:format
URL parameters
[edit]You want to use a URL parameter like this:
Then you can do something along these lines in the controller.
# GET /things/events/1
# GET /things/events/1.xml
def events
x = params[:x].to_i
# use x to compute some interesting 'earliest' and 'latest'
@diagnostics = earliest.to_s + " to " + latest.to_s
@things = \
Thing.find(:all, :conditions => \
[ "id = ? and start >= ? and start < ?", \
params[:id], earliest, latest])
respond_to do |format|
format.html # events.html.erb
format.xml { render :xml => @things }
end
end
The console
[edit]Go into your project directory and type
ruby script/console
This will allow you to interact in irb-like fashion with the classes in your Rails app. You can create instances of models. You can also use a very cool GDB-like debugger called ruby-debug[15], but in order to do that in Ubuntu, you need to have done a little bit of preparation.
sudo aptitude install ruby1.8-dev
sudo gem install ruby-debug
The best thing to do next is to put the method call "debugger" in your source code as a sort of breakpoint. Then run your code in the console and when it reaches this point, you'll pop right into ruby-debug. An example might look like this:
def events
x = params[:x].to_i
debugger # now you can examine x
....
end
You can print expressions in the debugger by typing "p <expr>". You can see the lines of source code by typing "list". You can see all the other available commands by typing "help", and find out about any specific command by typing "help <command>".
- It would be really useful here to show an example debugging session, especially one that really uncovers some unexpected problem.
On Weds 3/24/2010, I went to the Metrowest Ruby Users Group[16] and there was some discussion of the best strategy for developing Rails apps. The recommendation was to build models and tests for models first, get all that stuff debugged in the console, and then write views. This ran counter to what I had been thinking in developing my Django app. I had been thinking that if I mocked up my views first, it would force me to think about my database schemas, and then I'd build the models and business logic after the schemas were done.
I think that recommendation makes plenty of sense if you've already got your head around what your schemas should look like. Maybe it's tolerable to make a relatively brief early diversion into the views, just to clarify one's thinking about schemas. So maybe I'm not so far off target as I would have thought.
Ajax
[edit]Let's suppose you have a database table, where each row has a start
field of type datetime
, and it's natural for the user to want to look at the rows whose start times fall within a particular span of time.
On each Ajax request, the client requests a subset of table rows specified by a time span defined by variables earliest
and latest
, a couple of Time
[17] objects. The goal on the server side is to package these into an XML file and send them back to the client. The client's job is to receive the XML, parse it in Javascript, and handle presentation to the user.
The code in the controller looks like this.
# GET /entries/events/1
# GET /entries/events/1.xml
def events
# calculate earliest and latest, whatever that entails
@_begin = "%02d/%02d/%04d" % [earliest.mon, earliest.day, earliest.year]
@_end = "%02d/%02d/%04d" % [latest.mon, latest.day, latest.year]
@entries =
Entry.find(:all, :conditions =>
[ "id = ? and start >= ? and start < ?",
params[:id], earliest, latest])
respond_to do |format|
format.html # events.html.erb
format.xml # events.xml.erb
end
end
This method is used to populate an embedded-Ruby XML file, events.xml.erb
.
<?xml version='1.0' encoding='utf-8'?>
<events>
<begin><%=h @_begin %></begin>
<end><%=h @_end %></end>
<% @entries.each { |entry| %>
<entry>
<start><%=h entry.start %></start>
<description><%=h entry.description %></description>
... other fields ...
</entry>
<% } %>
</events>
Javascript initiates each Ajax transaction, and then sorts out the details of presentation, using jQuery[18][19] to parse the XML. The selected rows from the database table are stored as Javascript arrays in the list _events
. Depending on your application, it might instead make sense to do the presentation immediately in the Ajax callback.
var _events;
$.ajaxSetup({ cache: false });
function startAjaxTransaction() {
$.ajax({
url: '/entries/events/609440379.xml',
data: 'foo=bar&baz=xyzzy',
context: document.body,
success: function(data, textStatus, XMLHttpRequest) {
parseXml(data);
}
});
}
function parseXml(xml)
{
var _begin, _end;
_events = [ ];
$(xml).find("begin")
.each(function() { _begin = $(this).text(); });
$(xml).find("end")
.each(function() { _end = $(this).text(); });
$(xml).find("event")
.each(function() {
var e = { };
var fields = ['start', 'description', ....];
for (var i in fields) {
var f = fields[i];
e[f] = $(this).find(f).text();
}
_events.push(e);
});
document.getElementById("begindiv").innerHTML = _begin;
document.getElementById("enddiv").innerHTML = _end;
}
Prototype, Scriptaculous, etc
[edit]Hmm, looking at some of comments on a Ruby blog[20], it looks like the Ruby-community-preferred thing to do is to forego jQuery in favor of Prototype. Sorry, I tried that, and doing tooltips without jQuery is a nightmare.
Rails makes available some Javascript libraries including Prototype and Script.aculo.us. My tinkering with them has been minimal. In poking around the web, these were the resources that looked likely to be useful.
- http://weblog.rubyonrails.org/2007/11/7/prototype-1-6-0-and-script-aculo-us-1-8-0-released
- http://blog.codahale.com/2006/01/14/a-rails-howto-simplify-in-place-editing-with-scriptaculous/
- http://www.refreshaustin.org/presentations/prototype-scriptaculous-crash-course/
- http://prototypejs.org/learn
- http://particletree.com/features/quick-guide-to-prototype/
- http://www.oreillynet.com/xml/blog/2006/06/the_power_of_prototypejs_1.html
- http://www.sergiopereira.com/articles/prototype.js.html
- http://domscripting.com/book/contents/
- http://www.phpriot.com/articles/beginning-with-prototype/6
Dumb lessons, hard won
[edit]If an Ajax request isn't working as expected, always look at the XML that comes back in response.
Obviously, use Firefox's error console to look at Javascript errors, but it's also good to add a little JS logging of one's own. I do this in the HTML.
<div class="tabContent" id="jslog">
Log output:
</div>
and then I do this in Javascript:
function log(s) {
var area = document.getElementById("jslog");
area.innerHTML += "<br>" + JSON.stringify(s);
}
I find I am much more comfortable with Python than Ruby, and I'm thinking about switching to Django for the current project and coming back to Rails later, when I'm not trying to rush to get something working.
Notes
[edit]- ^ http://en.wikibooks.org/wiki/Ruby_on_Rails
- ^ http://www.pragprog.com/titles/rails3/agile-web-development-with-rails-third-edition
- ^ http://oreilly.com/catalog/9780596520717
- ^ http://wiki.rubyonrails.com/rails
- ^ http://rubyforge.org/projects/instantrails/
- ^ http://www.aptana.com
- ^ http://www.sapphiresteel.com/
- ^ http://subversion.tigris.org
- ^ http://tortoisesvn.net/downloads
- ^ http://railsbrain.com
- ^ http://docs.huihoo.com/ruby/ruby-man-1.4/syntax.html
- ^ http://api.rubyonrails.org/classes/ActiveRecord/Base.html
- ^ http://betterexplained.com/articles/intermediate-rails-understanding-models-views-and-controllers/
- ^ http://apidock.com/rails/ActiveRecord/Base/find/class
- ^ Information about ruby-debug
- ^ Metrowest Ruby Users Group
- ^ http://ruby-doc.org/core/classes/Time.html
- ^ http://docs.jquery.com/Main_Page
- ^ http://www.switchonthecode.com/tutorials/xml-parsing-with-jquery
- ^ http://weblog.rubyonrails.org/2007/11/7/prototype-1-6-0-and-script-aculo-us-1-8-0-released