aboutsummaryrefslogtreecommitdiffstats
path: root/railties/guides
diff options
context:
space:
mode:
authorJames Miller <james@jkmillertech.com>2010-02-03 15:27:27 -0800
committerJames Miller <james@jkmillertech.com>2010-02-03 15:27:27 -0800
commit6697a02ee362f82901d6f9278a2ef6341c781f66 (patch)
tree68551f49c399a8f3f8dc72ebb4eec0db8111e658 /railties/guides
parent8c2d245d5580cf42e74176a87e68f5cbceed794c (diff)
downloadrails-6697a02ee362f82901d6f9278a2ef6341c781f66.tar.gz
rails-6697a02ee362f82901d6f9278a2ef6341c781f66.tar.bz2
rails-6697a02ee362f82901d6f9278a2ef6341c781f66.zip
First pass at updating to Rails 3 AR query syntax
Diffstat (limited to 'railties/guides')
-rw-r--r--railties/guides/source/active_record_querying.textile167
1 files changed, 72 insertions, 95 deletions
diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile
index 302dad4f1a..4bfcea587c 100644
--- a/railties/guides/source/active_record_querying.textile
+++ b/railties/guides/source/active_record_querying.textile
@@ -11,6 +11,8 @@ This guide covers different ways to retrieve data from the database using Active
endprologue.
+WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in other versions of Rails.
+
If you're used to using raw SQL to find database records then, generally, you will find that there are better ways to carry out the same operations in Rails. Active Record insulates you from the need to use SQL in most cases.
Code examples throughout this guide will refer to one or more of the following models:
@@ -49,7 +51,21 @@ Active Record will perform queries on the database for you and is compatible wit
h3. Retrieving Objects from the Database
-To retrieve objects from the database, Active Record provides a class method called +Model.find+. This method allows you to pass arguments into it to perform certain queries on your database without the need of writing raw SQL.
+To retrieve objects from the database, Active Record provides several finder methods. These methods allows you to pass arguments into it to perform certain queries on your database without the need of writing raw SQL.
+
+The methods are:
+* +where+
+* +select+
+* +group+
+* +order+
+* +limit+
+* +joins+
+* +includes+
+* +lock+
+* +readonly+
+* +from+
+
+All of these methods return a Relation
Primary operation of <tt>Model.find(options)</tt> can be summarized as:
@@ -64,7 +80,7 @@ Active Record lets you retrieve a single object using three different ways.
h5. Using a Primary Key
-Using <tt>Model.find(primary_key, options = nil)</tt>, you can retrieve the object corresponding to the supplied _primary key_ and matching the supplied options (if any). For example:
+Using <tt>Model.find(primary_key)</tt>, you can retrieve the object corresponding to the supplied _primary key_ and matching the supplied options (if any). For example:
<ruby>
# Find the client with primary key (id) 10.
@@ -82,7 +98,7 @@ SELECT * FROM clients WHERE (clients.id = 10)
h5. +first+
-<tt>Model.first(options = nil)</tt> finds the first record matched by the supplied options. If no +options+ are supplied, the first matching record is returned. For example:
+<tt>Model.first</tt> finds the first record matched by the supplied options. For example:
<ruby>
client = Client.first
@@ -97,11 +113,9 @@ SELECT * FROM clients LIMIT 1
<tt>Model.first</tt> returns +nil+ if no matching record is found. No exception will be raised.
-NOTE: +Model.find(:first, options)+ is equivalent to +Model.first(options)+
-
h5. +last+
-<tt>Model.last(options = nil)</tt> finds the last record matched by the supplied options. If no +options+ are supplied, the last matching record is returned. For example:
+<tt>Model.last</tt> finds the last record matched by the supplied options. For example:
<ruby>
client = Client.last
@@ -116,13 +130,11 @@ SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
<tt>Model.last</tt> returns +nil+ if no matching record is found. No exception will be raised.
-NOTE: +Model.find(:last, options)+ is equivalent to +Model.last(options)+
-
h4. Retrieving Multiple Objects
h5. Using Multiple Primary Keys
-<tt>Model.find(array_of_primary_key, options = nil)</tt> also accepts an array of _primary keys_. An array of all the matching records for the supplied _primary keys_ is returned. For example:
+<tt>Model.find(array_of_primary_key)</tt> also accepts an array of _primary keys_. An array of all the matching records for the supplied _primary keys_ is returned. For example:
<ruby>
# Find the clients with primary keys 1 and 10.
@@ -138,26 +150,6 @@ SELECT * FROM clients WHERE (clients.id IN (1,10))
<tt>Model.find(array_of_primary_key)</tt> will raise an +ActiveRecord::RecordNotFound+ exception unless a matching record is found for <strong>all</strong> of the supplied primary keys.
-h5. Find all
-
-<tt>Model.all(options = nil)</tt> finds all the records matching the supplied +options+. If no +options+ are supplied, all rows from the database are returned.
-
-<ruby>
-# Find all the clients.
-clients = Client.all
-=> [#<Client id: 1, name: => "Lifo">, #<Client id: 10, name: => "Ryan">, #<Client id: 221, name: => "Russel">]
-</ruby>
-
-And the equivalent SQL is:
-
-<sql>
-SELECT * FROM clients
-</sql>
-
-<tt>Model.all</tt> returns an empty array +[]+ if no matching record is found. No exception will be raised.
-
-NOTE: +Model.find(:all, options)+ is equivalent to +Model.all(options)+
-
h4. Retrieving Multiple Objects in Batches
Sometimes you need to iterate over a large set of records. For example to send a newsletter to all users, to export some data, etc.
@@ -166,14 +158,14 @@ The following may seem very straight forward at first:
<ruby>
# Very inefficient when users table has thousands of rows.
-User.all.each do |user|
+User.each do |user|
NewsLetter.weekly_deliver(user)
end
</ruby>
But if the total number of rows in the table is very large, the above approach may vary from being under performant to just plain impossible.
-This is because +User.all+ makes Active Record fetch _the entire table_, build a model object per row, and keep the entire array in the memory. Sometimes that is just too many objects and demands too much memory.
+This is because +User.each+ makes Active Record fetch _the entire table_, build a model object per row, and keep the entire array in the memory. Sometimes that is just too many objects and demands too much memory.
h5. +find_each+
@@ -232,16 +224,16 @@ The +find+ method allows you to specify conditions to limit the records returned
h4. Pure String Conditions
-If you'd like to add conditions to your find, you could just specify them in there, just like +Client.first(:conditions => "orders_count = '2'")+. This will find all clients where the +orders_count+ field's value is 2.
+If you'd like to add conditions to your find, you could just specify them in there, just like +Client.where("orders_count = '2'")+. This will find all clients where the +orders_count+ field's value is 2.
-WARNING: Building your own conditions as pure strings can leave you vulnerable to SQL injection exploits. For example, +Client.first(:conditions => "name LIKE '%#{params[:name]}%'")+ is not safe. See the next section for the preferred way to handle conditions using an array.
+WARNING: Building your own conditions as pure strings can leave you vulnerable to SQL injection exploits. For example, +Client.where("name LIKE '%#{params[:name]}%'")+ is not safe. See the next section for the preferred way to handle conditions using an array.
h4. Array Conditions
Now what if that number could vary, say as an argument from somewhere, or perhaps from the user's level status somewhere? The find then becomes something like:
<ruby>
-Client.first(:conditions => ["orders_count = ?", params[:orders]])
+Client.where(["orders_count = ?", params[:orders]])
</ruby>
Active Record will go through the first element in the conditions value and any additional elements will replace the question marks +(?)+ in the first element.
@@ -249,7 +241,7 @@ Active Record will go through the first element in the conditions value and any
Or if you want to specify two conditions, you can do it like:
<ruby>
-Client.first(:conditions => ["orders_count = ? AND locked = ?", params[:orders], false])
+Client.where(["orders_count = ? AND locked = ?", params[:orders], false])
</ruby>
In this example, the first question mark will be replaced with the value in +params[:orders]+ and the second will be replaced with the SQL representation of +false+, which depends on the adapter.
@@ -257,13 +249,13 @@ In this example, the first question mark will be replaced with the value in +par
The reason for doing code like:
<ruby>
-Client.first(:conditions => ["orders_count = ?", params[:orders]])
+Client.where(["orders_count = ?", params[:orders]])
</ruby>
instead of:
<ruby>
-Client.first(:conditions => "orders_count = #{params[:orders]}")
+Client.where("orders_count = #{params[:orders]}")
</ruby>
is because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database *as-is*. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
@@ -275,7 +267,7 @@ h5. Placeholder Conditions
Similar to the +(?)+ replacement style of params, you can also specify keys/values hash in your array conditions:
<ruby>
-Client.all(:conditions =>
+Client.where(
["created_at >= :start_date AND created_at <= :end_date", { :start_date => params[:start_date], :end_date => params[:end_date] }])
</ruby>
@@ -286,7 +278,7 @@ h5. Range Conditions
If you're looking for a range inside of a table (for example, users created in a certain timeframe) you can use the conditions option coupled with the +IN+ SQL statement for this. If you had two dates coming in from a controller you could do something like this to look for a range:
<ruby>
-Client.all(:conditions => ["created_at IN (?)",
+Client.where(["created_at IN (?)",
(params[:start_date].to_date)..(params[:end_date].to_date)])
</ruby>
@@ -308,7 +300,7 @@ h5. Time and Date Conditions
Things can get *really* messy if you pass in Time objects as it will attempt to compare your field to *every second* in that range:
<ruby>
-Client.all(:conditions => ["created_at IN (?)",
+Client.where(["created_at IN (?)",
(params[:start_date].to_date.to_time)..(params[:end_date].to_date.to_time)])
</ruby>
@@ -329,14 +321,14 @@ Where _query_ is the actual query used to get that error.
In this example it would be better to use greater-than and less-than operators in SQL, like so:
<ruby>
-Client.all(:conditions =>
+Client.where(
["created_at > ? AND created_at < ?", params[:start_date], params[:end_date]])
</ruby>
You can also use the greater-than-or-equal-to and less-than-or-equal-to like this:
<ruby>
-Client.all(:conditions =>
+Client.where(
["created_at >= ? AND created_at <= ?", params[:start_date], params[:end_date]])
</ruby>
@@ -351,13 +343,13 @@ NOTE: Only equality, range and subset checking are possible with Hash conditions
h5. Equality Conditions
<ruby>
-Client.all(:conditions => { :locked => true })
+Client.where({ :locked => true })
</ruby>
The field name does not have to be a symbol it can also be a string:
<ruby>
-Client.all(:conditions => { 'locked' => true })
+Client.where({ 'locked' => true })
</ruby>
h5. Range Conditions
@@ -365,7 +357,7 @@ h5. Range Conditions
The good thing about this is that we can pass in a range for our fields without it generating a large query as shown in the preamble of this section.
<ruby>
-Client.all(:conditions => { :created_at => (Time.now.midnight - 1.day)..Time.now.midnight})
+Client.where({ :created_at => (Time.now.midnight - 1.day)..Time.now.midnight})
</ruby>
This will find all clients created yesterday by using a +BETWEEN+ SQL statement:
@@ -381,7 +373,7 @@ h5. Subset Conditions
If you want to find records using the +IN+ expression you can pass an array to the conditions hash:
<ruby>
-Client.all(:conditions => { :orders_count => [1,3,5] })
+Client.where({ :orders_count => [1,3,5] })
</ruby>
This code will generate SQL like this:
@@ -390,22 +382,6 @@ This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
</sql>
-h3. Find Options
-
-Apart from +:conditions+, +Model.find+ takes a variety of other options via the options hash for customizing the resulting record set.
-
-<ruby>
-Model.find(id_or_array_of_ids, options_hash)
-Model.find(:last, options_hash)
-Model.find(:first, options_hash)
-
-Model.first(options_hash)
-Model.last(options_hash)
-Model.all(options_hash)
-</ruby>
-
-The following sections give a top level overview of all the possible keys for the +options_hash+.
-
h4. Ordering
To retrieve records from the database in a specific order, you can specify the +:order+ option to the +find+ call.
@@ -413,37 +389,37 @@ To retrieve records from the database in a specific order, you can specify the +
For example, if you're getting a set of records and want to order them in ascending order by the +created_at+ field in your table:
<ruby>
-Client.all(:order => "created_at")
+Client.order("created_at")
</ruby>
You could specify +ASC+ or +DESC+ as well:
<ruby>
-Client.all(:order => "created_at DESC")
+Client.order("created_at DESC")
# OR
-Client.all(:order => "created_at ASC")
+Client.order("created_at ASC")
</ruby>
Or ordering by multiple fields:
<ruby>
-Client.all(:order => "orders_count ASC, created_at DESC")
+Client.order("orders_count ASC, created_at DESC")
</ruby>
h4. Selecting Specific Fields
By default, <tt>Model.find</tt> selects all the fields from the result set using +select *+.
-To select only a subset of fields from the result set, you can specify the subset via +:select+ option on the +find+.
+To select only a subset of fields from the result set, you can specify the subset via the +select+ method.
-NOTE: If the +:select+ option is used, all the returning objects will be "read only":#readonly-objects.
+NOTE: If the +select+ method is used, all the returning objects will be "read only":#readonly-objects.
<br />
For example, to select only +viewable_by+ and +locked+ columns:
<ruby>
-Client.all(:select => "viewable_by, locked")
+Client.select("viewable_by, locked")
</ruby>
The SQL query used by this find call will be somewhat like:
@@ -463,17 +439,17 @@ Where +&lt;attribute&gt;+ is the attribute you asked for. The +id+ method will n
You can also call SQL functions within the select option. For example, if you would like to only grab a single record per unique value in a certain field by using the +DISTINCT+ function you can do it like this:
<ruby>
-Client.all(:select => "DISTINCT(name)")
+Client.select("DISTINCT(name)")
</ruby>
h4. Limit and Offset
-To apply +LIMIT+ to the SQL fired by the +Model.find+, you can specify the +LIMIT+ using +:limit+ and +:offset+ options on the find.
+To apply +LIMIT+ to the SQL fired by the +Model.find+, you can specify the +LIMIT+ using +limit+ and +offset+ methods on the relation.
-If you want to limit the amount of records to a certain subset of all the records retrieved you usually use +:limit+ for this, sometimes coupled with +:offset+. Limit is the maximum number of records that will be retrieved from a query, and offset is the number of records it will start reading from from the first record of the set. For example:
+If you want to limit the amount of records to a certain subset of all the records retrieved you usually use +limit+ for this, sometimes coupled with +offset+. Limit is the maximum number of records that will be retrieved from a query, and offset is the number of records it will start reading from from the first record of the set. For example:
<ruby>
-Client.all(:limit => 5)
+Client.limit(5)
</ruby>
This code will return a maximum of 5 clients and because it specifies no offset it will return the first 5 clients in the table. The SQL it executes will look like this:
@@ -482,10 +458,10 @@ This code will return a maximum of 5 clients and because it specifies no offset
SELECT * FROM clients LIMIT 5
</sql>
-Or specifying both +:limit+ and +:offset+:
+Or chaining both +limit+ and +offset+:
<ruby>
-Client.all(:limit => 5, :offset => 5)
+Client.limit(5).offset(5)
</ruby>
This code will return a maximum of 5 clients and because it specifies an offset this time, it will return these records starting from the 5th client in the clients table. The SQL looks like:
@@ -496,12 +472,12 @@ SELECT * FROM clients LIMIT 5, 5
h4. Group
-To apply +GROUP BY+ clause to the SQL fired by the +Model.find+, you can specify the +:group+ option on the find.
+To apply +GROUP BY+ clause to the SQL fired by the finder, you can specify the +group+ method on the find.
For example, if you want to find a collection of the dates orders were created on:
<ruby>
-Order.all(:group => "date(created_at)", :order => "created_at")
+Order.group("date(created_at)").order("created_at")
</ruby>
And this will give you a single +Order+ object for each date where there are orders in the database.
@@ -519,7 +495,7 @@ SQL uses +HAVING+ clause to specify conditions on the +GROUP BY+ fields. You can
For example:
<ruby>
-Order.all(:group => "date(created_at)", :having => ["created_at > ?", 1.month.ago])
+Order.group("date(created_at)".having(["created_at > ?", 1.month.ago])
</ruby>
The SQL that would be executed would be something like this:
@@ -532,18 +508,18 @@ This will return single order objects for each day, but only for the last month.
h4. Readonly Objects
-To explicitly disallow modification/destruction of the matching records returned by +Model.find+, you could specify the +:readonly+ option as +true+ to the find call.
+To explicitly disallow modification/destruction of the matching records returned in a Relation object, you could chain the +readonly+ method as +true+ to the find call.
Any attempt to alter or destroy the readonly records will not succeed, raising an +ActiveRecord::ReadOnlyRecord+ exception. To set this option, specify it like this:
<ruby>
-Client.first(:readonly => true)
+Client.first.readonly(true)
</ruby>
If you assign this record to a variable client, calling the following code will raise an +ActiveRecord::ReadOnlyRecord+ exception:
<ruby>
-client = Client.first(:readonly => true)
+client = Client.first.readonly(true)
client.locked = false
client.save
</ruby>
@@ -676,7 +652,7 @@ Now all of the following will produce the expected join queries using +INNER JOI
h5. Joining a Single Association
<ruby>
-Category.all :joins => :posts
+Category.joins(:posts)
</ruby>
This produces:
@@ -689,7 +665,7 @@ SELECT categories.* FROM categories
h5. Joining Multiple Associations
<ruby>
-Post.all :joins => [:category, :comments]
+Post.joins(:category, :comments)
</ruby>
This produces:
@@ -703,13 +679,13 @@ SELECT posts.* FROM posts
h5. Joining Nested Associations (Single Level)
<ruby>
-Post.all :joins => {:comments => :guest}
+Post.joins(:comments => :guest)
</ruby>
h5. Joining Nested Associations (Multiple Level)
<ruby>
-Category.all :joins => {:posts => [{:comments => :guest}, :tags]}
+Category.joins(:posts => [{:comments => :guest}, :tags])
</ruby>
h4. Specifying Conditions on the Joined Tables
@@ -718,14 +694,14 @@ You can specify conditions on the joined tables using the regular "Array":#array
<ruby>
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
-Client.all :joins => :orders, :conditions => {'orders.created_at' => time_range}
+Client.joins(:orders).where('orders.created_at' => time_range)
</ruby>
An alternative and cleaner syntax to this is to nest the hash conditions:
<ruby>
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
-Client.all :joins => :orders, :conditions => {:orders => {:created_at => time_range}}
+Client.joins(:orders).where(:orders => {:created_at => time_range})
</ruby>
This will find all clients who have orders that were created yesterday, again using a +BETWEEN+ SQL expression.
@@ -750,12 +726,12 @@ This code looks fine at the first sight. But the problem lies within the total n
<strong>Solution to N <plus> 1 queries problem</strong>
-Active Record lets you specify all the associations in advanced that are going to be loaded. This is possible by specifying the +:include+ option of the +Model.find+ call. By +:include+, Active Record ensures that all the specified associations are loaded using minimum possible number of queries.
+Active Record lets you specify all the associations in advanced that are going to be loaded. This is possible by specifying the +include+ method of the +Model.find+ call. By +include+, Active Record ensures that all the specified associations are loaded using minimum possible number of queries.
Revisiting the above case, we could rewrite +Client.all+ to use eager load addresses:
<ruby>
-clients = Client.all(:include => :address, :limit => 10)
+clients = Client.include(:address).limit(10)
clients.each do |client|
puts client.address.postcode
@@ -772,12 +748,12 @@ SELECT addresses.* FROM addresses
h4. Eager Loading Multiple Associations
-Active Record lets you eager load any possible number of associations with a single +Model.find+ call by using an array, hash, or a nested hash of array/hash with the +:include+ option.
+Active Record lets you eager load any possible number of associations with a single +Model.find+ call by using an array, hash, or a nested hash of array/hash with the +include+ method.
h5. Array of Multiple Associations
<ruby>
-Post.all :include => [:category, :comments]
+Post.include(:category, :comments)
</ruby>
This loads all the posts and the associated category and comments for each post.
@@ -785,14 +761,14 @@ This loads all the posts and the associated category and comments for each post.
h5. Nested Associations Hash
<ruby>
-Category.find 1, :include => {:posts => [{:comments => :guest}, :tags]}
+Category.find(1).include(:posts => [{:comments => :guest}, :tags])
</ruby>
The above code finds the category with id 1 and eager loads all the posts associated with the found category. Additionally, it will also eager load every posts' tags and comments. Every comment's guest association will get eager loaded as well.
h4. Specifying Conditions on Eager Loaded Associations
-Even though Active Record lets you specify conditions on the eager loaded associations just like +:joins+, the recommended way is to use ":joins":#joining-tables instead.
+Even though Active Record lets you specify conditions on the eager loaded associations just like +joins+, the recommended way is to use "joins":#joining-tables instead.
h3. Dynamic Finders
@@ -889,10 +865,10 @@ Which will execute:
SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
</sql>
-You can also use +:include+ or +:joins+ for this to do something a little more complex:
+You can also use the +include+ or +joins+ methods for this to do something a little more complex:
<ruby>
-Client.count(:conditions => "clients.first_name = 'Ryan' AND orders.status = 'received'", :include => "orders")
+Client.count.where("clients.first_name = 'Ryan' AND orders.status = 'received'").include("orders")
</ruby>
Which will execute:
@@ -957,5 +933,6 @@ h3. Changelog
"Lighthouse ticket":http://rails.lighthouseapp.com/projects/16213-rails-guides/tickets/16
+* February 3, 2010: Update to Rails 3 by "James Miller":credits.html#bensie
* February 7, 2009: Second version by "Pratik":credits.html#lifo
* December 29 2008: Initial version by "Ryan Bigg":credits.html#radar