40

I have two models in a has_many relationship such that Log has_many Items. Rails then nicely sets up things like: some_log.items which returns all of the associated items to some_log. If I wanted to order these items based on a different field in the Items model is there a way to do this through a similar construct, or does one have to break down into something like:

Item.find_by_log_id(:all,some_log.id => "some_col DESC")
Bryan Ward
  • 6,443
  • 8
  • 37
  • 48

6 Answers6

75

There are multiple ways to do this:

If you want all calls to that association to be ordered that way, you can specify the ordering when you create the association, as follows:

class Log < ActiveRecord::Base
  has_many :items, :order => "some_col DESC"
end

You could also do this with a named_scope, which would allow that ordering to be easily specified any time Item is accessed:

class Item < ActiveRecord::Base
  named_scope :ordered, :order => "some_col DESC"
end

class Log < ActiveRecord::Base
  has_many :items
end

log.items # uses the default ordering
log.items.ordered # uses the "some_col DESC" ordering

If you always want the items to be ordered in the same way by default, you can use the (new in Rails 2.3) default_scope method, as follows:

class Item < ActiveRecord::Base
  default_scope :order => "some_col DESC"
end
Greg Campbell
  • 15,182
  • 3
  • 44
  • 45
  • 12
    Since Rails 3.x, the named_scope syntax is slightly different. It is now called using "scope" instead of "named_scope", and uses functions to define the scope structure. For instance : "scope :ordered, order("some_col DESC")". – Pierre-Adrien Nov 16 '11 at 20:17
  • 14
    In Rails 4 there’s another approach again. The default association scope should be specified as a lambda like `has_many :items, ->{ order(:some_col).where(foo: 'bar') }` and, similarly, named scopes now take a lambda `scope :name_of_scope, ->{ where(foo: 'bar') }`. The default scope takes a block: `default_scope: { where(foo: 'bar') }` – Leo Oct 24 '13 at 15:32
  • Superb answer. +1 – sscirrus Mar 05 '18 at 21:37
16

rails 4.2.20 syntax requires calling with a block:

class Item < ActiveRecord::Base
  default_scope { order('some_col DESC') }
end

This can also be written with an alternate syntax:

default_scope { order(some_col: :desc) }
random-forest-cat
  • 33,652
  • 11
  • 120
  • 99
4

Either of these should work:

Item.all(:conditions => {:log_id => some_log.id}, :order => "some_col DESC")
some_log.items.all(:order => "some_col DESC")
erik
  • 6,406
  • 3
  • 36
  • 36
3

set default_scope in your model class

class Item < ActiveRecord::Base
  default_scope :order => "some_col DESC"
end

This will work

Sayuj
  • 7,464
  • 13
  • 59
  • 76
0

order by direct relationship has_many :model

is answered here by Aaron

order by joined relationship has_many :modelable, through: :model

class Tournament
  has_many :games # this is a join table
  has_many :teams, through: :games

  # order by :name, assuming team has this column
  def teams
    super.order(:name)
  end
end

Tournament.first.teams # are returned ordered by name
ToTenMilan
  • 582
  • 1
  • 9
  • 19
0

For anyone coming across this question using more recent versions of Rails, the second argument to has_many has been an optional scope since Rails 4.0.2. Examples from the docs (see scopes and options examples) include:

has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }
has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy

As previously answered, you can also pass a block to has_many. "This is useful for adding new finders, creators and other factory-type methods to be used as part of the association." (same reference - see Extensions).

The example given there is:

has_many :employees do
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

In more modern Rails versions the OP's example could be written:

class Log < ApplicationRecord
  has_many :items, -> { order(some_col: :desc) }
end

Keep in mind this has all the downsides of default scopes so you may prefer to add this as a separate method:

class Log < ApplicationRecord
  has_many :items

  def reverse_chronological_items
    self.items.order(date: :desc)
  end
end
Matthew
  • 1,300
  • 12
  • 30