How do I automatically sort a has_many relationship in Rails?

Ruby on-Rails

Ruby on-Rails Problem Overview


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?

Ruby on-Rails Solutions


Solution 1 - Ruby on-Rails

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.

Solution 2 - Ruby on-Rails

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

Solution 3 - Ruby on-Rails

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.

Solution 4 - Ruby on-Rails

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

Solution 5 - Ruby on-Rails

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

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionBrian ArmstrongView Question on Stackoverflow
Solution 1 - Ruby on-RailsJim PulsView Answer on Stackoverflow
Solution 2 - Ruby on-RailsMatt SandersView Answer on Stackoverflow
Solution 3 - Ruby on-RailsnitecoderView Answer on Stackoverflow
Solution 4 - Ruby on-Railsvrish88View Answer on Stackoverflow
Solution 5 - Ruby on-RailsMax L.View Answer on Stackoverflow