Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 654
Justin Tanner Avatar asked Aug 03 '10 05:08

Justin Tanner


4 Answers

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.

like image 157
23 revs, 4 users 82% Avatar answered Oct 21 '22 03:10

23 revs, 4 users 82%


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 use default_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!

like image 20
Paul Oliver Avatar answered Oct 21 '22 03:10

Paul Oliver


You can use default_scope to implement a default sort order http://api.rubyonrails.org/classes/ActiveRecord/Scoping/Default/ClassMethods.html

like image 6
Slobodan Kovacevic Avatar answered Oct 21 '22 02:10

Slobodan Kovacevic


The accepted answer used to be correct - default scopes were the only way to do this. However, default scopes are actually chained (the scope itself is applied to any further scopes/calls), and this can cause some unpredictable behavior. Default scopes are thus widely considered something to avoid at almost all cost.

It's important to note that given your original question, default scope doesn't actually quite satisfy this, since in order to actually skip the default scope, any model has to explicitly specify .unscoped first. That means that if order(:something) is called, without .unscoped, the result is actually more similar to order(:default).order(:something). :something takes priority, sure, but :default is still there, so now there's a multicolumn sort, which might not be desired. The original question specifies that the default sort order be ignored if another order is called, and default scope, without .unscoped, doesn't meet that requirement.

Since Rails 6, however, there is now implicit_order_column, which can be set on a model.

class Book < ApplicationRecord
  self.implicit_order_column = 'publish_date'
end

This will cause the model to order by that column by default, instead of the primary key. Like the default, built in sort (which uses id), however, if a different order is specified (with order(:something)), this implicit ordering is ignored, not chained: .order(:something) does not result in a multicolumn sort, and the need for .unscoped is gone.

One of the most common uses of this is when switching to UUID's as primary keys. By default, Rails will still order on the primary key, but since that key is now a meaningless byte string, this order is now similarly meaningless. Thus to replicate the old behavior, such that SomeModel.last should return the most recently created record by default, simply set this on ApplicationRecord:

class ApplicationRecord < ActiveRecord::Base
  self.implicit_order_column = 'created_at'
end

Note that it is theoretically possible to cause an error here. created_at is not unique - it's stored with microsecond precision, but in theory it's possible to create to records with the exact same created_at time, and thus, any returned results that depended only on that would be non-deterministic - subsequent calls might return different records. It is a low enough chance, however, that it's often considered safe to do this - particularly if the code does not depend on a deterministic result here (which, it probably shouldn't - if deterministic results are needed, referencing the primary key or some other unique column is better when possible).

It's also worth noting that created_at (or whatever column is used), by default, does not have a key. This means selects will slightly slow down doing this, at least until a key is added to that column to fix this.

Finally, implicit_order_column has limited functionality - it is not possible to set even the sort order, much less to do more complex things like multi-column sorts. If that functionality is required, default_scope is still the way only to go.

like image 6
mschultz Avatar answered Oct 21 '22 04:10

mschultz