Rails: Default sort order for a rails model?
I would like to specify a default sort order in my model.
So that when I do a .where()
without specifying an .order()
it uses the default sort. But if I specify an .order()
, it overrides the default.
Solution 1:
default_scope
This works for Rails 4+:
class Book < ActiveRecord::Base
default_scope { order(created_at: :desc) }
end
For Rails 2.3, 3, you need this instead:
default_scope order('created_at DESC')
For Rails 2.x:
default_scope :order => 'created_at DESC'
Where created_at
is the field you want the default sorting to be done on.
Note: ASC is the code to use for Ascending and DESC is for descending (desc
, NOT dsc
!).
scope
Once you're used to that you can also use scope
:
class Book < ActiveRecord::Base
scope :confirmed, :conditions => { :confirmed => true }
scope :published, :conditions => { :published => true }
end
For Rails 2 you need named_scope
.
:published
scope gives you Book.published
instead of
Book.find(:published => true)
.
Since Rails 3 you can 'chain' those methods together by concatenating them with periods between them, so with the above scopes you can now use Book.published.confirmed
.
With this method, the query is not actually executed until actual results are needed (lazy evaluation), so 7 scopes could be chained together but only resulting in 1 actual database query, to avoid performance problems from executing 7 separate queries.
You can use a passed in parameter such as a date or a user_id (something that will change at run-time and so will need that 'lazy evaluation', with a lambda, like this:
scope :recent_books, lambda
{ |since_when| where("created_at >= ?", since_when) }
# Note the `where` is making use of AREL syntax added in Rails 3.
Finally you can disable default scope with:
Book.with_exclusive_scope { find(:all) }
or even better:
Book.unscoped.all
which will disable any filter (conditions) or sort (order by).
Note that the first version works in Rails2+ whereas the second (unscoped) is only for Rails3+
So
... if you're thinking, hmm, so these are just like methods then..., yup, that's exactly what these scopes are!
They are like having def self.method_name ...code... end
but as always with ruby they are nice little syntactical shortcuts (or 'sugar') to make things easier for you!
In fact they are Class level methods as they operate on the 1 set of 'all' records.
Their format is changing however, with rails 4 there are deprecation warning when using #scope without passing a callable object. For example scope :red, where(color: 'red') should be changed to scope :red, -> { where(color: 'red') }
.
As a side note, when used incorrectly, default_scope can be misused/abused.
This is mainly about when it gets used for actions like where
's limiting (filtering) the default selection (a bad idea for a default) rather than just being used for ordering results.
For where
selections, just use the regular named scopes. and add that scope on in the query, e.g. Book.all.published
where published
is a named scope.
In conclusion, scopes are really great and help you to push things up into the model for a 'fat model thin controller' DRYer approach.
Solution 2:
A quick update to Michael's excellent answer above.
For Rails 4.0+ you need to put your sort in a block like this:
class Book < ActiveRecord::Base
default_scope { order('created_at DESC') }
end
Notice that the order statement is placed in a block denoted by the curly braces.
They changed it because it was too easy to pass in something dynamic (like the current time). This removes the problem because the block is evaluated at runtime. If you don't use a block you'll get this error:
Support for calling #default_scope without a block is removed. For example instead of
default_scope where(color: 'red')
, please usedefault_scope { where(color: 'red') }
. (Alternatively you can just redefine self.default_scope.)
As @Dan mentions in his comment below, you can do a more rubyish syntax like this:
class Book < ActiveRecord::Base
default_scope { order(created_at: :desc) }
end
or with multiple columns:
class Book < ActiveRecord::Base
default_scope { order({begin_date: :desc}, :name) }
end
Thanks @Dan!
Solution 3:
You can use default_scope to implement a default sort order http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Default/ClassMethods.html