MTJ Hax

Rails named_scope gotchas

Posted in ruby on rails by mtjhax on April 29, 2010

There are two terrible gotchas in Rails named_scopes. These two have bitten many a named_scope newbie on the rear (including myself, except that my unit tests caught the problems before they went anywhere but my desktop, w00t for TDD!)

#1. Use length instead of size with the results returned by named_scopes.

Under certain conditions, particularly when using :limit in a named_scope, size and length will return different values. Example:

class Book < ActiveRecord::Base
  named_scope :top, lambda { |limit| {
    :limit => limit,
    :order => 'total_sales DESC'
    }}
end
# imagine we have 10 books with various values for total_sales...
>> Book.top(5).length
=> 5
>> Book.top(5).size
=> 10
# for some reason this only affects named_scopes:
>> Book.find(:all, :limit => 5).length
=> 5
>> Book.find(:all, :limit => 5).size
=> 5
# Go figure!

#2. named_scopes and AR calculations don’t mix

Named scopes are a joy to use, especially when you chain them. It’s completely addictive being able to use constructs like:

topten = Book.fiction.category('vampire').top(10)

(Hypothetical example, I do not read vampire novels…).

The arrays returned by named_scope are ActiveRecord arrays and support calculations such as .count and .maximum, so it’s tempting to want to try something like:

max_fiction_price = Book.top(10).maximum(:price)

Beware! You may not get the results you are looking for — in my experience, these will not work consistently. The problem is that ActiveRecord generates a query something like the following:

SELECT MAX(price) from books ORDER BY price DESC LIMIT 10;

As far as I can tell in researching it, this is not really valid SQL (at least not according to MySQL). LIMIT happens after everything else–similar to HAVING–and only filters the number of output rows that are displayed. Since the MAX() function tells SQL to aggregate the results into a single column, the ORDER and LIMIT clauses are meaningless.

Sometimes this type of statement works for me. I have tried it on SQLite3 and several different versions of MySQL for Linux and Windows. The database sometimes seems to understand that I want to order by price, take the top 10, then calculate the max price. It is entirely possible that in tests where the result appears to be correct, it is actually because the records were randomly ordered in a way that produced the correct result, but purely by chance.

Of course, Rails 3.0 is on the way and there may end up being changes in these behaviors since AR is being rebuilt from the ground up. Whatever the case, avoid mixing conditions and limits, and make sure your unit and functional tests are written to validate the number of results your named_scopes return.

Advertisements