http://asciicasts.com/episodes/202-active-record-queries-in-rails-3
202: Active Record Queries in Rails 3 (view original Railscast)
Other formats:
Over the last two episodes we’ve shown you how to set up your computer for Rails 3 and create new Rails 3 applications. In this episode we’ll begin looking at some of its new features, starting with ActiveRecord which provides a new interface for performing database queries. Pratik Naik went into this subject in detail in a post on his blog recently which is well worth reading.
Some Basic Examples
To start we’ll show you a few examples of old ActiveRecord find calls and convert them into the new query format. For this we’ll be using a basic Rails application that has two models: Article
and Comment
that have a relationship whereby an Article
has_many :comments
.
The first find we’ll update returns the ten most recently published articles.
- Article.find(:all, :order => "published_at desc", :limit => 10)
The basic approach to converting an ActiveRecord query to the new Rails 3 format is to look at the hash of options that’s being passed to find
and to replace each item in the hash with an equivalent method. So, instead of the find call above we can use:
- Article.order("published_at desc").limit(10)
As you can see the new syntax is easy to convert from the old Rails find but has a neater syntax.
The old hash options don’t always map exactly onto the new methods however as we’ll demonstrate in this next example.
- Article.find(:all, :conditions => ["published_at <= ?", Time.now], :include => :comments)
There are only two real exceptions to the rule and conveniently the example above uses them both. The find above will get all of the articles that have a published date before the current time along with any associated comments. In Rails 3 this becomes:
- Article.where("published_at <= ?", Time.now).includes(:comments)
Instead of :conditions
we now use the where
method, passing in the same arguments as we would to :conditions
. The arguments can be passed as an array but it’s cleaner to pass them separately. For getting associated records :include
gets pluralized to become the includes
method. All of the other options we’d normally pass to find
become methods with the same name as the option.
Our final example fetches the most recently published article.
- Article.find(:first, :order => "published_at desc")
Using the Rails 3 syntax this becomes:
- Article.order("published_at desc").first()
Note that we don’t call first
until the end of the method chain.
As we’re fetching in descending order we could rewrite the line above as:
- Article.order("published_at").last()
This will perform the same query but with slightly more concise code.
In Rails 3.0 we can use either the old find
methods or the new Rails 3 syntax but in Rails 3.1 the old methods will be deprecated and from Rails 3.2 they will be removed completely. It’s well worth rewriting your finds as you migrate your applications to Rails 3 so that your applications will be compatible with future releases of Rails 3.
You might be wondering at this point just what the point of this new syntax is, especially as it will break a lot of existing Rails applications when they are upgraded. Well there is a purpose to this change and it lies in the power of lazy loading.
Lazy Loading
To demonstrate lazy loading we’ll use our application’s console. If we ask for all of the articles we’ll get an array returned as we’d expect and we’ll see that we have three articles in our database.
ruby-1.9.1-p378 > Article.all => [#<Article id: 1, name: "It's Ancient", published_at: nil, hidden: false, created_at: "2010-02-22 20:35:42", updated_at: "2010-02-22 20:35:42">, #<Article id: 2, name: "Can't See Me", published_at: nil, hidden: false, created_at: "2010-02-22 20:37:03", updated_at: "2010-02-22 20:37:03">, #<Article id: 3, name: "To the Future!", published_at: nil, hidden: false, created_at: "2010-02-22 20:38:17", updated_at: "2010-02-22 20:38:17">]
If we want to get all of the articles in alphabetical order we can do so by using the order method:
ruby-1.9.1-p378 > articles = Article.order("name") => #<ActiveRecord::Relation:0x00000101669b90 @table=#<Arel::Table:0x000001023e9af8 @name="articles", @options={:engine=>#<Arel::Sql::Engine:0x000001023a15c8 @ar=ActiveRecord::Base, @adapter_name="SQLite">}, @engine=#<Arel::Sql::Engine:0x000001023a15c8 @ar=ActiveRecord::Base, @adapter_name="SQLite">, …
Instead of a list of articles being returned this time we have an ActiveRecord::Relation
object. This object stores information about our find, but the database query hasn’t yet been made. This is what is meant by lazy loading in this context: the data isn’t loaded until it has to be. If we were to enumerate through the records with each
or get all or just the first of the articles then the query will be made.
ruby-1.9.1-p378 > articles.first => #<Article id: 2, name: "Can't See Me", published_at: nil, hidden: false, created_at: "2010-02-22 20:37:03", updated_at: "2010-02-22 20:37:03">
Now let’s see how this applies to our application. We’ve generated a scaffold for the article model so we have an articles controller with the usual seven actions. The code for the index
action uses Article.all
to get all of the articles immediately:
/app/controllers/articles_controller.rb
- def index
- @articles = Article.all
- respond_to do |format|
- format.html # index.html.erb
- format.xml { render :xml => @articles }
- end
- end
If we use use any of the find options on the articles, such as ordering by name then an ActiveRecord::Relation
object will be returned instead and the query will not be performed in the controller. The database query will instead be performed in the view code when we enumerate through each Article
.
/app/views/articles/index.html.erb
- <% @articles.each do |article| %>
- <tr>
- <td><%= article.name %></td>
- <td><%= article.published_at %></td>
- <td><%= article.hidden %></td>
- <td><%= link_to 'Show', article %></td>
- <td><%= link_to 'Edit', edit_article_path(article) %></td>
- <td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td>
- </tr>
- <% end %>
If we load the index page now the articles will be shown in alphabetical order.
The nice thing about this is that if you’re using fragment caching with the cache
method in your view this will now work better as the database query will not be performed unless it is necessary.
The new query syntax makes it easier to build up find conditions. Let’s say we want to filter the articles so that only the hidden ones are shown if we have hidden=1
in the URL’s query string. We can do that by modifying the index
action like this:
/app/controllers/articles_controller.rb
- def index
- @articles = Article.order('name')
- if params[:hidden]
- @articles = @articles.where(:hidden =>(params[:hidden] == "1"))
- end
- respond_to do |format|
- format.html # index.html.erb
- format.xml { render :xml => @articles }
- end
- end
Now we check that the there is a hidden
parameter passed and if there is we add a where
method to the find that will show only the hidden articles if that hidden parameter has a value of 1
. If we append that parameter to the URL and reload the page we’ll see just the hidden articles.
Likewise if we pass 0
we’ll see only the visible articles.
Being able to chain together methods like this is a nice way to be able to build up more complex database queries while knowing that the query won’t actually be executed until the data is needed.
Named Scopes
Next we’ll show you some of the changes to named scopes in Rails 3. Below is our Article
model with two named scopes, one to fetch the visible articles and one to fetch the articles that have been published.
/app/models/article.rb
- class Article < ActiveRecord::Base
- named_scope :visible, :conditions => ["hidden != ?", true]
- named_scope :published, lambda { {:conditions => ["published_at <= ?", Time.zone.now]} }
- end
These named scopes are defined as we’d define them in a Rails 2 application but the Rails 3 approach is a little different. The first difference is that the method to define a named scope is no longer named_scope
but just scope
. Also we no longer pass the conditions as a hash but, as with find, we use now use methods. Like we did with the new find methods we use where
instead of :conditions
. In the Rails 3 syntax the named scopes will look like this:
/app/models/article.rb
- class Article < ActiveRecord::Base
- scope :visible, where("hidden != ?", true)
- scope :published, lambda { where("published_at <= ?", Time.zone.now) }
- end
Another new feature is the ability to build up scopes. If we want to create a scope called recent
that will return the recently published visible articles ordered by their publish date we can do so by reusing the two scopes we already have.
- scope :recent, visible.published.order("published_at desc")
What we’ve done in our new scope is chain together the two scopes we already have an add an order
method to create a new scope and this chaining ability is a very powerful feature to have when creating scopes for our models.
We can try our new named scope in the console. If we call Article.recent
an ActiveRecord::NamedScope::Scope
object is returned. This is an object that behaves in a similar way to the ActiveRecord::Relation
object we saw earlier.
ruby-1.9.1-p378 > Article.recent => #<ActiveRecord::NamedScope::Scope:0x0000010318bd08 @table=#<Arel::Table:0x00000102740ea8 @name="articles", @options={:engine=>#<Arel::Sql::Engine:0x00000102651900 @ar=ActiveRecord::Base>}, @engine=#<Arel::Sql::Engine:0x00000102651900 @ar=ActiveRecord::Base>>, …
If we call all
on the Scope
object then we’ll see the matching article returned.
ruby-1.9.1-p378 > Article.recent.all => [#<Article id: 1, name: "It's Ancient", published_at: "2010-01-01", hidden: false, created_at: "2010-02-22 20:35:42", updated_at: "2010-02-22 23:00:16">]
A Final Tip
We’ll round up this episode with a useful tip. If you have a Relation
or Scope
object and want to see the SQL query that it would run against the database you can call to_sql
on it.
ruby-1.9.1-p378 > Article.recent.to_sql => "SELECT \"articles\".* FROM \"articles\" WHERE (hidden != 't') AND (published_at <= '2010-02-22 22:47:12.023289') ORDER BY published_at desc"
This shows the SQL that ActiveRecord will perform to return the recently published articles that are not hidden.
That’s it for this episode on using ActiveRecord queries in Rails 3. There are a lot of great additions that will make the code in your Rails