Skip to content

Commit

Permalink
Updated guide, adding to the conditions section mostly.
Browse files Browse the repository at this point in the history
  • Loading branch information
radar committed Oct 5, 2008
1 parent f2a713c commit 1f4bed7
Showing 1 changed file with 87 additions and 4 deletions.
91 changes: 87 additions & 4 deletions railties/doc/guides/activerecord/finders.txt
Expand Up @@ -103,6 +103,56 @@ Be aware that `Client.first`/`Client.find(:first)` and `Client.last`/`Client.fin

If you'd like to add conditions to your find, you could just specify them in there, just like `Client.find(:first, :conditions => "orders_count = '2'")`. Now what if that number could vary, say as a parameter from somewhere, or perhaps from the user's level status somewhere? The find then becomes something like `Client.find(:first, :conditions => ["orders_count = ?", params[:orders]])`. ActiveRecord will go through the first element in the conditions value and any additional elements will replace the question marks (?) in the first element. If you want to specify two conditions, you can do it like `Client.find(:first, :conditions => ["orders_count = ? AND locked = ?", params[:orders], false])`. In this example, the first question mark will be replaced with the value in params orders and the second will be replaced with true and this will find the first record in the table that has '2' as its value for the orders_count field and 'false' for its locked field.

The reason for doing code like:

[source, ruby]
`Client.find(:first, :conditions => ["orders_count = ?", params[:orders]])`

instead of:

`Client.find(:first, :conditions => "orders_count = #{params[:orders]}")`

is because of parameter safety. Putting the variable directly into the conditions string will parse the variable *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 parameters directly inside the conditions string.

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 we had two dates coming in from a controller we could do something like this to look for a range:

[source, ruby]
Client.find(:all, :conditions => ["created_at IN (?)", (params[:start_date].to_date)..(params[:end_date].to_date)])

This would generate the proper query which is great for small ranges but not so good for larger ranges. For example if you pass in a range of date objects spanning a year that's 365 (or possibly 366, depending on the year) strings it will attempt to match your field against.

[source, sql]
SELECT * FROM `users` WHERE (created_at IN ('2007-12-31','2008-01-01','2008-01-02','2008-01-03','2008-01-04','2008-01-05','2008-01-06','2008-01-07','2008-01-08','2008-01-09','2008-01-10','2008-01-11','2008-01-12','2008-01-13','2008-01-14','2008-01-15','2008-01-16','2008-01-17','2008-01-18','2008-01-19','2008-01-20','2008-01-21','2008-01-22','2008-01-23',...
2008-12-15','2008-12-16','2008-12-17','2008-12-18','2008-12-19','2008-12-20','2008-12-21','2008-12-22','2008-12-23','2008-12-24','2008-12-25','2008-12-26','2008-12-27','2008-12-28','2008-12-29','2008-12-30','2008-12-31'))


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:

[source, ruby]
Client.find(:all, :conditions => ["created_at IN (?)", (params[:start_date].to_date.to_time)..(params[:end_date].to_date.to_time)])

[source, sql]
SELECT * FROM `users` WHERE (created_at IN ('2007-12-01 00:00:00', '2007-12-01 00:00:01' ... '2007-12-01 23:59:59', '2007-12-02 00:00:00'))

This could possibly cause your database server to raise an unexpected error, for example MySQL will throw back this error:

[source, txt]
Got a packet bigger than 'max_allowed_packet' bytes: <query>

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:

[source, ruby]
Client.find(:all, :condtions => ["created_at > ? AND created_at < ?", params[:start_date], params[:end_date]])

You can also use the greater-than-or-equal-to and less-than-or-equal-to like this:

[source, ruby]
Client.find(:all, :condtions => ["created_at >= ? AND created_at <= ?", params[:start_date], params[:end_date]])

Just like in Ruby.

== Ordering

If you're getting a set of records and want to force an order, you can use `Client.find(:all, :order => "created_at")` which by default will sort the records by ascending order. If you'd like to order it in descending order, just tell it to do that using `Client.find(:all, :order => "created_at desc")`
Expand Down Expand Up @@ -133,6 +183,8 @@ SELECT * FROM clients LIMIT 5, 5

== Group

TODO

== Read Only

Readonly is a find option that you can set in order to make that instance of the record read-only. Any attempt to alter or destroy the record will not succeed, raising an `ActiveRecord::ReadOnlyRecord` error. To set this option, specify it like this:
Expand All @@ -149,18 +201,28 @@ client.save

== Lock

TODO

== Making It All Work Together

You can chain these options together in no particular order as ActiveRecord will write the correct SQL for you. For example you could do this: `Client.find(:all, :order => "created_at DESC", :select => "viewable_by, created_at", :conditions => ["viewable_by = ?", params[:level]], :limit => 10), which should execute a query like `SELECT viewable_by, created_at FROM clients WHERE ORDER BY created_at DESC LIMIT 0,10` if you really wanted it.
You can chain these options together in no particular order as ActiveRecord will write the correct SQL for you. For example you could do this:

[source, ruby]
Client.find(:all, :order => "created_at DESC", :select => "viewable_by, created_at", :conditions => ["viewable_by = ?", params[:level]], :limit => 10, :include => :orders, :joins => :address)

Which should execute a query like

[source, sql]
example goes here

== Eager Loading

Eager loading is loading associated records along with any number of records in as few queries as possible. Lets say for example if we wanted to load all the addresses associated with all the clients all in the same query we would use `Client.find(:all, :include => :address)`. If we wanted to include both the address and mailing address for the client we would use `Client.find(:all), :include => [:address, :mailing_address]). Inclue will first find the client records and then load the associated address records. Running script/server in one window, and executing the code through script/console in another window, the output should look similar to this:

[source, sql]
Client Load (0.000383) SELECT \* FROM clients
Address Load (0.119770) SELECT addresses.\* FROM addresses WHERE (addresses.client_id IN (13,14))
MailingAddress Load (0.001985) SELECT mailing_addresses.\* FROM mailing_addresses WHERE (mailing_addresses.client_id IN (13,14))
Client Load (0.000383) SELECT * FROM clients
Address Load (0.119770) SELECT addresses.* FROM addresses WHERE (addresses.client_id IN (13,14))
MailingAddress Load (0.001985) SELECT mailing_addresses.* FROM mailing_addresses WHERE (mailing_addresses.client_id IN (13,14))

The numbers `13` and `14` in the above SQL are the ids of the clients gathered from the `Client.find(:all)` query. Rails will then run a query to gather all the addresses and mailing addresses that have a client_id of 13 or 14. Although this is done in 3 queries, this is more efficient than not eager loading because without eager loading it would run a query for every time you called `address` or `mailing_address` on one of the objects in the clients array, which may lead to performance issues if you're loading a large number of records at once.

Expand Down Expand Up @@ -189,6 +251,10 @@ client = Client.find_or_initialize_by_name('Ryan')

will either assign an existing client object with the name 'Ryan' to the client local variable, or initialize new object similar to calling `Client.new(:name => 'Ryan')`. From here, you can modify other fields in client by calling the attribute setters on it: `client.locked = true` and when you want to write it to the database just call `save` on it.

== Finding By SQL

TODO

== Working with Associations

When you define a has_many association on a model you get the find method and dynamic finders also on that association. This is helpful for finding associated records within the scope of an exisiting record, for example finding all the orders for a client that have been sent and not received by doing something like `Client.find(params[:id]).orders.find_by_sent_and_received(true, false)`. Having this find method available on associations is extremely helpful when using nested controllers.
Expand Down Expand Up @@ -237,6 +303,18 @@ end
This will work with `Client.recent(2.weeks.ago)` and `Client.recent` with the latter always returning records with a created_at date between right now and 2 weeks ago.

Remember that named scopes are stackable, so you will be able to do `Client.recent(2.weeks.ago).unlocked` to find all clients created between right now and 2 weeks ago and have their locked field set to false.

== Existance of Objects

TODO

== Calculations

TODO

== With Scope

TODO

== Credits

Expand All @@ -258,3 +336,8 @@ Thanks to Mike Gunderloy for his tips on creating this guide.
1. Did section on limit and offset, as well as section on readonly.
2. Altered formatting so it doesn't look bad.


=== Sunday, 05 October 2008
1. Extended conditions section to include IN and using operators inside the conditions.
2. Extended conditions section to include paragraph and example of parameter safety.
3. Added TODO sections.

0 comments on commit 1f4bed7

Please sign in to comment.