homeASCIIcasts

152: Rails 2.3 extras 

(view original Railscast)

Rails 2.3 is now very near to release, so in this episode we’re going to show some more of its new features. In the last four episodes we’ve already covered some of the big new features in Rails 2.3; this one will wrap up this short series with an summary of some of the smaller new additions.

Find in Batches

The first thing we’re going to demonstrate is find_in_batches. Dealing with large numbers of ActiveRecord objects can place a large burden of your server’s memory; find_in_batches will, as its name suggests, return the objects in batches reducing the amount of memory used. By default, find_in_batches works with batches of 1,000 records, but we’re going to demonstrate it on a smaller scale. For each command in script/console that we run we’ll show the SQL that Rails generates below.

We have a Product model that has 25 items in it. If we get the items in batches of ten then four calls are made to the database.

>> Product.find_in_batches(:batch_size => 10)  do |batch|
?>   puts "Products in batch: #{batch.size}"
>> end
Products in batch: 10
Products in batch: 10
Products in batch: 5
  Product Load (1.1ms)   SELECT * FROM "products" WHERE (products.id >= 0) ORDER BY products.id ASC LIMIT 10
  Product Load (1.0ms)   SELECT * FROM "products" WHERE (products.id > 10) ORDER BY products.id ASC LIMIT 10
  Product Load (0.6ms)   SELECT * FROM "products" WHERE (products.id > 20) ORDER BY products.id ASC LIMIT 10
  Product Load (0.1ms)   SELECT * FROM "products" WHERE (products.id > 25) ORDER BY products.id ASC LIMIT 10

Only the records needed for each batch are fetched from the database which places less of a demand on the server’s memory.

There is a similar method called each which can also be used to iterate over a large collection of items. It differs from find_in_batches in that the object in the block is a single item rather than a batch.

>> Product.each(:batch_size => 10) do |product|
?>   puts product.name
>> end
Book
Cactus
Camera
Car
Chair
(and 20 other items)

The same four database calls are made as before.

  Product Load (1.2ms)   SELECT * FROM "products" WHERE (products.id >= 0) ORDER BY products.id ASC LIMIT 10
  Product Load (1.0ms)   SELECT * FROM "products" WHERE (products.id > 10) ORDER BY products.id ASC LIMIT 10
  Product Load (0.6ms)   SELECT * FROM "products" WHERE (products.id > 20) ORDER BY products.id ASC LIMIT 10
  Product Load (0.1ms)   SELECT * FROM "products" WHERE (products.id > 25) ORDER BY products.id ASC LIMIT 10

Although we’ve used find_in_batches for retrieving a small number of items it is designed to work with large amounts of data and is meant to be used when you’re dealing with thousands of records at a time.

scoped_by_x

The next few feature is related to named scopes. Named scopes are a great addition to Rails, but sometimes it can be a pain to have to generate a named scope for each column in a model. Rails 2.3 provides a scoped_by_ method which acts in a similar way to find_by_ in that it takes a column name. For example, to create a scope that returns the products that cost £4.99 we can use

>> p = Product.scoped_by_price(4.99).first
=> #<Product id: 1, category_id: 1, name: "Book", price: #<BigDecimal:2097bb0,'0.499E1',8(8)>, description: "Paperback book", created_at: "2009-03-13 20:47:15", updated_at: "2009-03-13 22:12:48">
  Product Load (0.4ms)   SELECT * FROM "products" WHERE ("products"."price" = 4.99) LIMIT 1

Note that the database query isn’t performed when the scoped_by_ method is called, but when the method after (in this case first) is.

Scopes can be chained together, so if we want to find the first product in category 1 with a price of £4.99 we can run the following code.

>> p = Product.scoped_by_price(4.99).scoped_by_category_id(1).first
=> #<Product id: 1, category_id: 1, name: "Book", price: #<BigDecimal:2086478,'0.499E1',8(8)>, description: "Paperback book", created_at: "2009-03-13 20:47:15", updated_at: "2009-03-13 22:12:48">
    Product Load (0.8ms)   SELECT * FROM "products" WHERE (("products"."category_id" = 1 AND "products"."price" = 4.99)) ORDER BY price ASC LIMIT 1

Default Scope

The next new addition is default_scope. It provides a way to define default find conditions or ordering for a model. While it wouldn’t be a particularly useful thing to do, we could define a default scope for our Product model that only returns products that cost £4.99.

class Product < ActiveRecord::Base
  belongs_to :category
  default_scope :conditions => ["price = ?", 4.99]
end

If we try to get all of our products now, only the ones costing £4.99 will be returned.

>> Product.count
=> 9
 SQL (0.3ms)   SELECT count(*) AS count_all FROM "products" WHERE (price = 4.99)

This isn’t the best use for default_scope as we don’t always want to just find products costing £4.99. If our Product model had a deleted_at column we could define a default scope to return only the items that had not been marked as deleted. That way we can safely ‘delete’ a product by just setting the deleted_at column to the current time.

Instead of defining default conditions a more useful option for default_scope is to provide a default order. If we always want our products to come back in order of price we can set the default scope to do that.

class Product < ActiveRecord::Base
  belongs_to :category
  default_scope :order => ["price ASC"]
end

There may be a slight loss of performance for requests when the order isn’t important, but if we want a model to be returned in a certain order most of the time this is still a good use for default_scope.

Try

A try method is added to all Ruby objects in Rails 2.3. As its name indicates it allows you to try calling a method on an object, even though that object may be nil.

We have a product that costs £4.99 in our products table so we can retrieve its name. If we try to get the name of a product with a price of £4.95 when none of our products are priced at £4.95 an error will be thrown as we’re trying to call name on nil.

>> Product.find_by_price(4.99).name
=> "Book"
>> Product.find_by_price(4.95).name
NoMethodError: You have a nil object when you didn't expect it!
The error occurred while evaluating nil.name
	from (irb):2
>>

The try method will try to call a method on an object but return nil if that object doesn’t respond to that method. It takes a symbol as an argument.

>> Product.find_by_price(4.99).try(:name)
=> "Book"
>> Product.find_by_price(4.95).try(:name)
=> nil
>> 

Render

There have been some great improvements made to the render method in Rails 2.3 which can be used to make view code more concise. On our products’ index page we’re rendering a list of products in a partial. In older versions of Rails we’d have to use this code.

<%= render :partial => 'product', :collection => @products %>

Later versions of Rails automatically assumed the name of the partial from the collection allowing us to shorten the code.

<%= render :partial => @products %>

In Rails 2.3 we can just pass the list of products and Rails will know that we want to render a partial for each one.

<%= render @products %>

Similarly, to render a single model, rather than a collection, we can just pass that object, allowing us to replace

<%= render :partial => ‘product’, :object => @product %>

with

<%= render @product %>

Similar improvements have been made in the controller code too. If we want to render an action, for example rendering new after an invalid attempt to create a new model, we currently use

render :action =>  'new'

In Rails 2.3 we can just pass the name of the action as a string.

render 'new'

If we want to render a template for a different controller we can pass both names separated by a slash.

render 'products/edit'

There are many more new features in Rails 2.3 than have been covered in this series. Take a look at the Rails 2.3 Release Notes for a comprehensive overview of what’s new in 2.3.