Rails: Default sort order for a rails model?

Viewed 164585

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.

4 Answers

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.

Related