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.