How do you order by a custom model method that has no attribute in SQL?
Previously I ordered my posts as this:
@posts = Post.find(:all, :order => "created_at DESC")
But now I want to replace created_at
with a custom method I wrote in the Post model that gives a number as its result.
My guess:
@posts = Post.find(:all, :order => "custom_met开发者_C百科hod DESC")
which fails..
It fails because you are asking your db to do the sorting.
@posts = Post.all.sort {|a,b| a.custom_method <=> b.custom_method}
Note that this becomes non-trivial when you want to start paging results and no longer wish to fetch .all
. Think about your design a bit before you go with this.
Just to expand on @Robbie's answer
Post.all.sort_by {|post| post.custom_method }.reverse
As the first answer noted, order is an Active Record command that essentially does a SQL query on your database, but that field doesn't actually exist in your database.
As someone else commented, you can more cleanly run the Ruby method sort_by by using the ampersand (more info here):
Post.all.sort_by(&:custom_method)
However, things do get complicated depending on what you want to do in your view. I'll share a case I recently did in case that helps you think through your problem. I needed to group my resource by another resource called "categories", and then sort the original resource by "netvotes" which was a custom model method, then order by name. I did it by:
- Ordering by name in the controller:
@resources = Resource.order(:name)
- Grouping by category in the outer loop of the view:
<% @resources.group_by(&:category).each do |category, resources| %>
- Then sorting the resources by votes in the partial for resources:
<%= render resources.sort_by(&:netvotes).reverse %>
The view is a bit confusing, so here is the full view loop in index.html.erb:
<% @resources.group_by(&:category).each do |category, resources| %>
<div class="well">
<h3 class="brand-text"><%= category.name %></h3>
<%= render resources.sort_by(&:netvotes).reverse %>
</div>
<% end %>
And here is the _resource.html.erb partial:
<div class="row resource">
<div class="col-sm-2 text-center">
<div class="vote-box">
<%= link_to fa_icon('chevron-up lg'), upvote_resource_path(resource), method: :put %><br>
<%= resource.netvotes %><br>
<%= link_to fa_icon('chevron-down lg'), downvote_resource_path(resource), method: :put %>
</div>
</div>
<div class="col-sm-10">
<%= link_to resource.name, resource.link, target: "_blank" %>
<p><%= resource.notes %></p>
</div>
</div>
This is a bit more complicated than what I like but this I like to keep my sort to stay as a active record model so its bit more complicated than just
Post.all.sort_by {|post| post.custom_method }
what I do is:
ids = Post.all.sort_by {|post| post.custom_method }.map(&:ids)
Post.for_ids_with_order(ids)
this is a custom scope in the Post model
#app/models/post.rb
class Post < ApplicationRecord
...
scope :for_ids_with_order, ->(ids) {
order = sanitize_sql_array(
["position(id::text in ?)", ids.join(',')]
)
where(:id => ids).order(order)
}
...
end
I hope that this help
Well, just Post.find(:all)
would return an array of AR objects. So you could use Array.sort_by and pass it a block, and since those records are already fetched, you can access the virtual attribute inside the block that sort_by takes.
RDoc: Enumerable.sort_by
Keep in mind that sort_by
will return an Array
, not an ActiveRecord::Relation
, which you might need for pagination or some other some view logic. To get an ActiveRecord::Relation
back, use something like this:
order_by_clause = Post.sanitize_sql_array(<<custom method expressed in SQL>>, <<parameters>>)
Post.all.order(Arel.sql(order_by_clause))
in rails 3 we can do this as: Post.order("custom_method DESC")
When upgrading app from rails2 to rails3
精彩评论