如何在 Rails 中自动对 has_many 关系排序?

这似乎是一个非常简单的问题,但我还没有看到它的任何地方回答。

如果你有:

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

你为什么不能用这样的东西来点评:

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

如果你需要大量引用它,命名作用域可以工作,甚至人们也可以这样做:

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

但直觉告诉我应该更简单,我错过了什么?

64856 次浏览

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

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.

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.

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

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