Sunday, August 17, 2008

Using jQuery on Rails in 3 easy steps

There are a couple of reasons why you may want to use jQuery in your Ruby on Rails' projects. It could be the shorter code. Or you may find it a little bit more unobstrusive than Prototype. Or, if you're like me, you may have been enticed by the number of plugins available for it.

So how do we use it then?

You can choose to make Prototype and jQuery work with each other, and effectively double the amount of javascript that your user downloads everytime he or she visits your site.

Or you can switch to jQuery altogether, which is what I'll be discussing next.

Download and install jQuery

This should be a no-brainer. Before we can use the darned thing, we have to download and install it first.

Download the latest version of jQuery from here and copy it to your public/javascripts directory. We won't be needing the Prototype/Script.aculo.us files anymore so you can delete them while you're at it.

Download and install jRails

Prototype and Script.aculo.us comes with their very own little Ruby helpers which makes life a little bit easier for us. Instead of writing:
<a href="#" onclick="new Ajax.Request('/person/4', {asynchronous:true, evalScripts:true, method:'delete'}); return false;">Destroy</a>
We simply write this instead:
<%= link_to_remote "Destroy", :url => person_url(:id => @person), :method => :delete %>
Here's another example:
new Form.Element.Observer('suggest', 0.25, function(element, value) {new Ajax.Updater('suggest', '/testing/find_suggestion', {asynchronous:true, evalScripts:true, parameters:'q=' + value})}) 
To this:
<%= observe_field :suggest, :url => { :action => :find_suggestion },
      :frequency => 0.25,
      :update => :suggest,
      :with => 'q'
      %>
Now that we're moving to jQuery, guess what? All those nice little helpers are now broken. But not for long though.

Introducing jRails:
jRails is a drop-in jQuery replacement for Prototype/Script.aculo.us on Rails. Using jRails, you can get all of the same default Rails helpers for javascript functionality using the lighter jQuery library.
Let's install it by running:
./script/plugin install http://ennerchi.googlecode.com/svn/trunk/plugins/jrails
Then include it in your layouts:
<%= javascript_include_tag "jquery", "jquery-ui", "jrails" %>
Or if you weren't lazy before and deleted your Prototype/Script.aculo.us files, and you're feeling a little lazy now:
<%= javascript_include_tag :all %>
Make a little modification on your previous Prototype/Script.aculo.us helper calls

You'll need to change all id paramaters in your helper calls from :id or 'id' to '#id'. For example, something like this:
<%= observe_field :suggest, :url => { :action => :find_suggestion },
      :frequency => 0.25,
      :update => :suggest,
      :with => 'q'
      %>
Needs to be changed to:
<%= observe_field '#suggest', :url => { :action => :find_suggestion },
      :frequency => 0.25,
      :update => '#suggest',
      :with => 'q'
      %> 
Or something like this:
render :update do |page|
  page.replace 'article', :partial => 'item'
end
To this:
render :update do |page|
  page.replace '#article', :partial => 'item'
end
Once you've made all the necessary changes in your code, that's it. You're done. Now fire up your web server and browser and see how it works.

Friday, August 15, 2008

ActsAsReference: A convenient method of handling belongs_to associations in Rails

In one of the projects I'm working on, I had a variety of models which needed a 'Company' field. Being the normalization and associations junkie that I am, I had them setup this way:
class Company < ActiveRecord::Base
end

class Truck < ActiveRecord::Base
  belongs_to :company 
end

class Vessel < ActiveRecord::Base
  belongs_to :company 
end

class Container < ActiveRecord::Base
  belongs_to :company
end

class Depot < ActiveRecord::Base
  belongs_to :company
end
Which is all good and well until I came to one of my form views:
<% form_for :truck, @truck, :url => { :action => "create" } do |f| %>
  <%= f.text_field :plate_no %>
  <%= select("truck", "company_id", Company.find(:all).collect {|c| [ c.name, c.id ] }, { :include_blank => true }) %>
  <%= submit_tag 'Create' %>
<% end %>
What we have is a text field for the truck's plate number and a select field for it's company. Except that there's no company yet to select, and even if there was, there's no way we can add another. And in the case of this project, it is a must that the user be able to easily add companies which the particular entity (truck, vessel, container, depot, cat, dog, fish) belongs to on the fly.

We can of course add a link that gives the user an option to create a new company, but that will be an akward solution and would be inconvenient for the user at the very least. What we need is an unobstrusive way for the user to enter the t company, as well as a robust solution which we can easily adapt in code.

For starters, let us try this:
<% form_for :truck, @truck, :url => { :action => "create" } do |f| %>
  <%= f.text_field :plate_no %>
  <%= text_field_tag :company_name %>
  <%= submit_tag 'Create' %>
<% end %> 
Then for our controller:
def create 
  @truck = Truck.new

  company_name = params[:company_name]
  company = Company.find_by_name(company_name)
  company ||= Company.create(:name => company_name)

  @truck.company = company
  @truck.update_attributes(params[:truck])
end
Which has to be repeated for our update action, and ALL the other views. Unobstrusive: yes. Robust: no.

This is where my acts_as_reference plugin comes into play.

Install it via git:
git submodule add git://github.com/ErolFornoles/acts_as_reference.git vendor/plugins/acts_as_reference
And then use it in code:
class Company < ActiveRecord::Base
  acts_as_reference :name, :create => true
end
<% form_for :truck, @truck, :url => { :action => "create" } do |f| %>
  <%= f.text_field :plate_no %>
  <%= f.text_field :company %>
  <%= submit_tag 'Create' %>
<% end %> 
def create
  @truck = Truck.new
  @truck.update_attributes(params[:truck])
end
Just a one-liner addition to our reference model. Simpler view code compared to the first one which used a select tag. And just the normal, plain, bland controller. Notice that our company field is now a simple text field which, uhmmm, accepts text. Let's drop in a little jQuery Autocomplete goodness and a few CSS custom styling, and we'll come up with something like this:



The user can type a new company, or auto-completion can guide him/her to typing or selecting an existing one.

Thursday, August 14, 2008

Editra: A free lightweight cross-platform editor

"Editra is a multi-platform text editor with an implementation that focuses on creating an easy to use interface and features that aid in code development. Currently it supports syntax highlighting and variety of other useful features for over 60 programming languages."

I've just started using this text editor for my Rails projects, and so far it looks promising. It's an improvement to SciTe and it's faster than JEdit or any of the IDE's that I have tried, specifically Netbeans, RadRails and Komodo. It also has a file-browser plugin which makes working with Rails projects manageable. Not to mention that it's totally free.

Wednesday, August 13, 2008

Optimize eager loading in Rails 2.1 using reference indexes

Rails 2.1 has a new eager loading scheme that works in a radically different way compared to previous versions. Instead of wrapping everything up in one humongous SQL statement composed of a complex and lengthy series of joins, the new mechanism breaks the eager loading call to multiple simple statements.

Let's say that we have three models:
class PetShop < ActiveRecord::Base
  has_many :dogs
end

class Dog < ActiveRecord::Base
  belongs_to :pet_shop
end

class Cats < ActiveRecord::Base
  belongs_to :pet_shop
end
In Rails 2.0, this:
PetShop.find(:all, :include => [:cats, :dogs])
Will yield this:
SELECT `pet_shops`.`id` AS t0_r0, `pet_shops`.`name` AS t0_r1, `pet_shops`.`created_at` AS t0_r2, `pet_shops`.`updated_at` AS t0_r3, `dogs`.`id` AS t1_r0, `dogs`.`name` AS t1_r1, `dogs`.`pet_shop_id` AS t1_r2, `dogs`.`created_at` AS t1_r3, `dogs`.`updated_at` AS t1_r4, `cats`.`id` AS t2_r0, `cats`.`name` AS t2_r1, `cats`.`pet_shop_id` AS t2_r2, `cats`.`created_at` AS t2_r3, `cats`.`updated_at` AS t2_r4 FROM `pet_shops` LEFT OUTER JOIN `dogs` ON dogs.pet_shop_id = pet_shops.id LEFT OUTER JOIN `cats` ON cats.pet_shop_id = pet_shops.id;
WTF?!

This is a cartesian join; if you had 100 petshops, with 100 dogs and 100 cats each, the number of rows returned will be 100 * 100 * 100 = 1,000,000. A million friggin' rows. Add another association, like say has_many :birds, and the count further skyrockets.

In Rails 2.1, the same call will result to:
SELECT * FROM `pet_shops`;
SELECT `dogs`.* FROM `dogs` WHERE (`dogs`.pet_shop_id IN (1,2,3,4,5,6,7,8,9,10));
SELECT `cats`.* FROM `cats` WHERE (`cats`.pet_shop_id IN (1,2,3,4,5,6,7,8,9,10));
Back to our assumption of having 100 petshops, with 100 dogs and 100 cats each. The number of rows returned is now 100 + 100(100) + 100(100) = 20,100.

There were reports though - unconfirmed, since I could not find a concrete article in the intertubes - that MySQL was taking a hit CPU-wise because of the additional queries. I have a different hunch though.

Assuming we had the "usual" migration:
create_table :pet_shops do |t|
  t.string :name
  t.timestamps
end

create_table :cats do |t|
  t.string :name
  t.references :pet_shop
  t.timestamps
end

create_table :dogs do |t|
  t.string :name
  t.references :pet_shop
  t.timestamps
end
We can analyze the SQL statements generated by Rails 2.1's eager loading mechanism by using MySQL's EXPLAIN command. We will not attempt to analyze the first statement since it is basically a fetch all.
EXPLAIN SELECT `dogs`.* FROM `dogs` WHERE (`dogs`.pet_shop_id IN (1,2,3,4,5,6,7,8,9,10));
EXPLAIN SELECT `cats`.* FROM `cats` WHERE (`cats`.pet_shop_id IN (1,2,3,4,5,6,7,8,9,10));
And here's what we got in return:
ALL - A full table scan is done for each combination of rows from the previous tables. This is normally not good if the table is the first table not marked const, and usually very bad in all other cases. Normally, you can avoid ALL by adding indexes that allow row retrieval from the table based on constant values or column values from earlier tables.
"Normally not good, and usually very bad in all other cases." The MySQL guys sure didn’t mince words here.

Let’s do what they suggested and add an index to our reference field:
add_index :dogs, :petshop_id
add_index :cats, :petshop_id
Or if you’re using the easier_indexes plugin, we can revise our migration:
create_table :cats do |t|
  t.string :name
  t.references :pet_shop, :index => true
  t.timestamps
end

create_table :dogs do |t|
  t.string :name
  t.references :pet_shop, :index => true
  t.timestamps
end 
Running the same EXPLAIN command this time yields us with:
range - Only rows that are in a given range are retrieved, using an index to select the rows.
I'm too lazy to do a benchmark now, so let me know how this one works for you.

About

This is my blog of assorted geekiness, mostly dealing with Ruby on Rails. Here I will write about hints, tips, tricks and hacks, as well as feature topics on how to get the most out of Ruby/Rails. I hope to meet people who have the same interests as me, specially those from the RoR community. If there's anything specific which you would like to ask or discuss, please leave a comment or contact me directly.