Brian Armstrong
Brian Armstrong

Reputation: 19863

How do I automatically sort a has_many relationship in Rails?

This seems like a really simple question but I haven't seen it answered anywhere.

In rails if you have:

class Article < ActiveRecord::Base 
  has_many :comments 
end 
class Comments < ActiveRecord::Base 
  belongs_to :article 
end

Why can't you order the comments with something like this:

@article.comments(:order=>"created_at DESC")

Named scope works if you need to reference it a lot and even people do stuff like this:

@article.comments.sort { |x,y| x.created_at <=> y.created_at }

But something tells me it should be simpler. What am I missing?

Upvotes: 114

Views: 71913

Answers (5)

Matt Sanders
Matt Sanders

Reputation: 10695

As of Rails 4, you would do:

class Article < ActiveRecord::Base 
  has_many :comments, -> { order(created_at: :desc) }
end 
class Comment < ActiveRecord::Base 
  belongs_to :article 
end

For a has_many :through relationship the argument order matters (it has to be second):

class Article
  has_many :comments, -> { order('postables.sort' :desc) }, 
           :through => :postable
end

If you will always want to access comments in the same order no matter the context you could also do this via default_scope within Comment like:

class Comment < ActiveRecord::Base 
  belongs_to :article 
  default_scope { order(created_at: :desc) }
end

However this can be problematic for the reasons discussed in this question.

Before Rails 4 you could specify order as a key on the relationship, like:

class Article < ActiveRecord::Base 
  has_many :comments, :order => 'created_at DESC'
end 

As Jim mentioned you can also use sort_by after you have fetched results although in any result sets of size this will be significantly slower (and use a lot more memory) than doing your ordering through SQL/ActiveRecord.

If you are doing something where adding a default order is cumbersome for some reason or you want to override your default in certain cases, it is trivial to specify it in the fetching action itself:

sorted = article.comments.order('created_at').all

Upvotes: 61

Max L.
Max L.

Reputation: 274

And if you need to pass some additional arguments like dependent: :destroy or whatever, you should append the ones after a lambda, like this:

class Article < ActiveRecord::Base 
  has_many :comments, -> { order(created_at: :desc) }, dependent: :destroy
end

Upvotes: 4

nitecoder
nitecoder

Reputation: 5486

If you are using Rails 2.3 and want to use the same default ordering for all collections of this object you can use default_scope to order your collection.

class Student < ActiveRecord::Base
  belongs_to :class

  default_scope :order => 'name'

end

Then if you call

@students = @class.students

They will be ordered as per your default_scope. TBH in a very general sense ordering is the only really good use of default scopes.

Upvotes: 7

Jim Puls
Jim Puls

Reputation: 81052

You can specify the sort order for the bare collection with an option on has_many itself:

class Article < ActiveRecord::Base 
  has_many :comments, :order => 'created_at DESC'
end 
class Comment < ActiveRecord::Base 
  belongs_to :article 
end

Or, if you want a simple, non-database method of sorting, use sort_by:

article.comments.sort_by &:created_at

Collecting this with the ActiveRecord-added methods of ordering:

article.comments.find(:all, :order => 'created_at DESC')
article.comments.all(:order => 'created_at DESC')

Your mileage may vary: the performance characteristics of the above solutions will change wildly depending on how you're fetching data in the first place and which Ruby you're using to run your app.

Upvotes: 159

vrish88
vrish88

Reputation: 21397

You can use ActiveRecord's find method to get your objects and sort them too.

  @article.comments.find(:all, :order => "created_at DESC")

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html

Upvotes: 6

Related Questions