This is definitely a work-in-progress: please do not use it as a start-to-finish guide. If there’s something wrong or obviously missing, you can contact me at rtimmons@u.washington.edu or “officially” via catalysthelp@u.washington.edu.
This is my very disorganized and almost completely without-explanation gathering of notes that will one day ferment into an online curriculum for Catalyst’s Ruby on Rails Workshop.
I learned Ruby on Rails mostly by trial and error and by following a few tutorials and reading the Rails API. Google has been my best friend in the process.
Before reading my notes, be sure to check out
Why’s Poignant Guide to Ruby for a good look at the Ruby programming language. A lot of Rails’s magic comes from how well Ruby is designed to fit this particular application. Read through chapter 5 at least before going too far with Rails.
Amy’s Getting Started With Rails Guide looks at RoR with a “I want to get started right away” kind of attitude, which is how some people prefer to get started: diving right in.
Also don’t forget to check out
Central to Rails is the MVC—Model View Controller—design pattern. This simple idea is used everywhere in modern programming. For a full treatise on the subject, read the wikipedia article: it does a very good job explaining the subject.
I’ve also tried to introduce MVC into my discussion of PHP and JavaScript, but using MVC with these technologies is actually more work than not using it; compared to using MVC with Rails, in which case it’s completely unavoidable.
Matz, the “father” of Ruby, has to say that
Everyone has an individual background. Someone may come from Python, someone else may come from Perl, and they may be surprised by different aspects of the language. Then they come up to me and say, ‘I was surprised by this feature of the language, so Ruby violates the principle of least surprise.’ Wait. Wait. The principle of least surprise is not for you only. The principle of least surprise means principle of least my surprise. And it means the principle of least surprise after you learn Ruby very well. For example, I was a C++ programmer before I started designing Ruby. I programmed in C++ exclusively for two or three years. And after two years of C++ programming, it still surprises me.
I come from languages like Java, C, Perl, and PHP with Python being my most adventurous leap into crazy syntax on the procedural side of programming and Scheme being the craziest on the functional side of programming. I stay curly-brace, white-space-don’t-matter with my code.
So Ruby’s syntax was a bit weird to me at first, but I’ve found that everything actually works exactly as I would have implemented had I written the Ruby implementation. (What a disaster that would have been, though…). My point is that Ruby does hide a lot from you, but it does so very predictably: rarely is there some weird syntax thing going on that you probably won’t be able to reduce to something similar in PHP or Java.
(This last concept I often compare to the difference between British English and American English: some things might take you off-guard at first, but it shouldn’t take you long to mentally switch between the two.)
What makes Ruby such a great language for a framework like Rails (and why similar projects like CakePHP in languages like PHP are so much more quirky) is that it, Ruby, is purely object-oriented and has the notion of dynamic classes. More on this later.
Ruby’s syntax is pretty lax. Semicolons, curly braces, and even parens are optional in most cases. Functions take comma-separated lists of arguments, but as soon as a key=>value pair is seen, that argument and the rest of the arguments are globbed together in a hash. So then
has_many(:comments, :dependent => :destroy, :order => "created_at ASC")
is equivalent to
has_many :comments, :dependent => :destroy, :order => "created_at ASC"
which is in turn equivalent to
has_many :comments, { :dependent => :destroy, :order => "created_at ASC" }
which is, of course, equivalent to
has_many(:comments, { :dependent => :destroy, :order => "created_at ASC" })
You’ll also see clever use of the asterisk array exploder, a la
Chapter.find( :all, :conditions => ['name = ? and title LIKE \'?\'', *conds] )
where conds is an array. This “unleashes” or “unwalls” the array, if you
will—it’s like you’re throwing the items in the array individually as
arguments to the function rather than sending them all together as a single
argument. Consider the following example:
def right_triangle(a,b) Math.sqrt(a*a+b*b) end some_args = [3,4] puts right_triangle(*some_args) # => 5
It’s meant to infer the “dereference” semantics from pointers in C, but that analogy is difficult to follow.
There is a lot of confusion, I’ve found, around the use of symbols, which are
preceded by colons (e.g., :comments) versus strings, which are double- or
single-quoted (e.g. "comments").
Technically strings are arrays of bytes which are both tedious to compare for equality and which also carry with them lots of extra emotional baggage. Symbols on the other hand are converted immediately by the Ruby compiler to integers and are very light and breezy, as they say.
(Ruby is a nice language in that it will allow you to convert a symbol to
a string, (:spam.to_s, for example, yields "spam"), but this operation is
expensive.)
The end result is that you should use symbols whenever you don’t need the user of your application (i.e. the lay-person using your fancy new Rails app) to see the actual string-like name of the symbol. If you could do a global search-and-replace on your symbol and replace it with some crazy other symbol and still have it compile and “make sense” (perhaps harder to read but still syntactically correct) then you should use a symbol.
The how and where of Rails.
Rails 2.0 is new as of December 17, 2007. A lot of its core libraries have been reworked to make things a lot faster and a lot simpler both for you and the server. The only problem, though, is that most code written in Rails 1.x will not work in Rails 2.x. Also, many of the tutorials, message threads, and books are all written for Rails 1.x.
If you’re pressing forward with Rails 2.0, it’s handy to have a list of what’s changed in the newest version. Please contact me for a PDF file containing a aptly-discussed list of the changes and how to use the new feature set of Rails 2.0.
Aptana Studio is based on Eclipse and has lots of good stuff. It’s cross-platform, open-source, and all-around pretty cool.
How to import an existing Rails project into Aptana
Rails is installed by default on Mac OS X (although you might want to download the most recent version if you’re still running 10.4 “Tiger” as it’s a little bit screwy; the version in 10.5 “Leopard” is fine.)
I’ve tried the very popular Locomotive application for one-button app creation and running, but I find that using the command line utilities are simply easier.
InstantRails includes MySQL, Apache, Ruby, and Rails all pre-configured and good to go. I’ve found that this is not as easy to set up and use as it should be, though. In particular, you have to explicitly create the apps using the command-line and then either configure the Apache instance by hand or look for the automatic port number.
I hate configuring Apache almost as much as I hate using the command line on Windows, which I hate a lot.
A good text editor is Notepad++ or Eclipse, the latter being a full-fledged IDE (integrated development environment) built in Java; you may have to get the Rails plugins if you opt for Eclipse.
I use TextMate (~$35 with free 30-day trial), which is a great Mac text editor with lots of built-in support for web and *nix stuff. I occasionally dabble in emacs on the command line (or Aquamacs Emacs, the “Mac Version” of emacs), but I rarely step outside of TextMate for anything text-related.
I’ve heard good things about the e Text Editor, which is supposed to be a TextMate clone for Windows. I’ve used it only briefly and found it to be a good editor. I’d look into it if the command line appeals to you in any way.
The console lets you interact with your models via the command line. This lets you quickly see if your Model logic is correct and to quickly query your database.
Running the app’s rails console is simply a matter of
$ script/console
which in turn gives access to the console. E.g.,
$ script/console >> c = Chapter.new(:title => "Blah") >> c.save true >> Chapter.find(:first)
The RoR Wiki Plugins Page has a good overview of the plugins and whatnot while http://techno-weenie.net has a lot of Rails Plugins listed.
To install a plugqin, find the (svn) address and run the plugin script with the install option. E.g.,
$ script/plugin install http://svn.techno-weenie.net/...
This results in the plugin code being placed into vendors/plugins. You don’t need to do anything to activate a plugin: it is automatically loaded and used by virtue of it residing in vendors/plugins.
This is how to create scaffolding in Rails 2.0. It is the functionality
originally found in Rails 1.x using script/generate scaffold_resource.
$ script/generate scaffold Chapter \
title:string \
teaser:text \
body:text \
position: integer \
created_at:datetime \
updated_at:datetime
This automatically creates the initial migration to bootstrap the database into action. Rails can’t create the actual database though: it can only create its tables. (The backslashes are there so you can break the line; you may omit them if you do not break the line between the fields.)
To create a migration, run e.g., $ script/generate migration add_price to
create a migration where we add a price field to the products model.
This creates a file db/migrate/001_add_price.rb (notice the conversion from under_scored to CamelCase):
class AddPrice < ActiveRecord::Migration def self.up # the first arg here, :products, is the table name add_column :products, :price, :decimal, :precision => 8, :scale => 2, :default => 0 end def self.down remove_column :products, :price end end
Use self.up to migrate to a new version and self.down method to rollback.
Run all the necessary migrations by running $ rake db:migrate from themain
project directory.
More migrations information here.
Use ? to indicate placeholders for count/find/etc operations that interact
with the database. This is sort of like C’s printf.
def self.count_by_date(year, month = nil, day = nil, limit = nil) if !year.blank? from, to = self.time_delta(year, month, day) Article.count( ["published_at BETWEEN ? AND ? AND published = ?", from, to, true] ) else Article.count( :conditions => {:published => true } ) end end
This is also the real only way to avoid SQL injection attacks, so be sure that if you’re ever querying against user data that use use this style of querying.
Rails automatically takes care of datetimes if the table has columns
created_at and/or updated_at.
For (most) fields you can call automagic find methods. E.g.,
Chapter.find_by_title "this is cool" Chapter.find_all_by_title "this is awesome"
The title part comes directly from the name of the column and isn’t
hard-coded into Rails. Note that the all method always returns an array of
objects, even if only one thing is found.
Also have find_or_create_by_*field*. E.g.,
Chapter.find_or_create_by_title "Chunky Bacon"
Aside from the automagic methods above, Rails also has the generic find
method:
Chapter.find( :all, :conditions => ["position > ?", 7], :group => "position", :order => "created_at ASC" :select => "title, position", # pulls only specific fields :include => :comments, # which associations to include :limit => 5 )
The ? is a placeholder that further arguments will fill in (in order if
there are more than one).
has_and_belongs_to_manyImagine you have something like
class Restaurant < ActiveRecord::Base has_and_belongs_to_many :food_items end
where each restaurant has a close_time and an open_time and you’d like to
allow the user to search for restaurants that are both open and serving
pancakes.
There are actually two ways to do this: an intuitive way and a fast way. Unfortunately the intuitive way is very slow and the fast way is kind of opaque.
The below will get you started, but it doesn’t cover how to limit the query to only restaurants that are currently open. That shouldn’t be too hard to add, though.
Using Rails’ built-in finder methods and then using a filter on the results.
This way is simpler and more intuitive. You simply have in your controller
def query @food_item = FoodItem.find(:first, :select => 'title', :conditions => [ 'title = ?', params[:food_item][:title] ]) @restaurants = Restaurant.find(:all, :conditions => { :title => params[:restaurant][:title]} @restaurants.delete_if { |rest| !rest.food_items.include? @food_item } end
This is, however, potentially very slow: we’re not using the database to do the heavy lifting it’s designed to do. Essentially we grab all the restaurants regardless of what food_items they have. We then simply remove the restaurants that don’t serve the food_item we found.
We have to do this because there is a table between the restaurant and its food_items: the join table that establishes the “has_and_belongs_to_many” relationship.
What’s interesting here is how we do in fact have to manually join our two tables together to achieve any reasonable amount of efficiency. Rails’s ActiveRecord can only be so smart.
Using a bit of custom SQL, which isn’t at all obvious but isn’t too tough:
def query # this part is the same: @food_item = FoodItem.find(:first, :select => 'title', :conditions => [ 'title = ?', params[:food_item][:title] ]) # this part is changed: @restaurants = Restaurant.find(:all, :joins => 'food_items_restaurants ON food_items_restaurants.restaurant_id = restaurants.id' :conditions => 'food_item_id = #{@food_item.id}') end
We actually could have written this as a more complex sql statement directly:
SELECT food_items.*, restaurants.* FROM food_items,food_items_restaurants,restaurants WHERE food_items.id = food_items_restaurants.food_item_id AND restaurants.id = food_items_restaurants.restaurant_id AND food_item.title = YOUR_FOOD_ITEM_ID_HERE
This breaks the “veil of plurality” in Rails: we had to say
restaurants.id when normally we refer to restaurant.id, but in this
case we’re working directly with the database table names, and since the
Restaurant model is stored in the restaurants database, we’re forced
to use the plural form in this scenario.
def self.time_delta(year, month = nil, day = nil) from = Time.mktime(year, month || 1, day || 1) to = from.next_year to = from.next_month unless month.blank? to = from + 1.day unless day.blank? to = to - 1 # pull off 1 second so we don't # overlap onto the next day return [from, to] end
A sample Model with validations:
class Product < ActiveRecord::Base validates_presence_of :title, :description, :image_url validates_numericality_of :price validates_uniqueness_of :title validates_format_of :image_url, :with => %r{[0-9]*}i # ^ this indicates a # regular expression in # ruby :message => "error message" protected # since we never need to access this directly # in the controller def validate if price.nil? or price < 0.00 then errors.add(:price,"error message") end end end
Calls methods before/after certain events take place. Useful for requiring authentication or logging events:
class Product < ActiveRecord:Base before_filter :login_required after_create :record_visit end
Including
has_many :comments
results in lots of magic methods:
chapter.comments chapter.has_comments chapter.comments.new chapter.comments << Comment.create # push operator
Call <%= yield %> in /app/views/layouts/application.rhtml to render the
object’s view.
Create, e.g., /app/views/layouts/notes.rhtml to create a layout that overrides the default ([…]/application.rhtml isn’t even touched).
Put layout "notes" in a controller to force a controller to use another
controller’s layout.
The file name starts with an underscore. E.g.
app/views/layouts/_footer.rhtml for the “footer” partial.
Use <%= render :partial => "layouts/footer" %> to render a partial or
<%= render :partial => "comment", :collection => @comments %>
to render a bunch of them
These both use GET:
First
link_to 'Next Page', :action => 'show', :id => product # The `id` in `product` is implicit.
Second
link_to 'Destroy', { :action => 'destroy', :id => product, :confirm => "Really?", :method => :post }
…while this uses POST:
button_to "Add to Cart", :action => :add_to_cart, :id => product
redirect_toUse redirect_to within a controller:
redirect_to notes_url(:action => 'show', :id => params[:id])
After first adding
map.notes 'notes/:action/:id', :controller => 'machines'
to routes.rb. In this URL, anything preceded by a colon is dynamic
Add
before_filter :login_required, :only => [:secret_stuff]
to the top of the controller. Then the login_required method will have to
return true before execution will continue. Can also use :except instead of
:only.
Use
$ rake db:sessions:create
to create the database migrations needed for database-stored sessions. You must also comment out the appropriate lines in config/environment.rb.
Sessions require having a session cookie key, set using, E.g.,
session :session_key => '_my_session_id_is_cool'
in controllers/application_controller.rb.
Author: Ryan Timmons
Last Modified: 06 August 2008 15:23:13 PDT
URL: http://uwwebpub.com/