37

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_method DESC")

which fails..

MZaragoza
  • 10,108
  • 9
  • 71
  • 116
Trip
  • 26,756
  • 46
  • 158
  • 277

7 Answers7

42

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.

jdl
  • 17,702
  • 4
  • 51
  • 54
  • 32
    Or slightly simpler you can use Post.all.sort_by(&:custom_method), achieves the same result but slightly easier on the eyes. – NZKoz Oct 29 '10 at 22:34
  • This doesn't seem to work if there are nils in you comparison field. Is there a work-around for this? I have the follow `display_rank` values [10, 20, 30, nil, nil, nil, nil, nil, nil] this statement `Event.find(607).event_staff_users.sort_by{|u| u.display_rank}` give this error `ArgumentError: comparison of NilClass with 30 failed`. I found that using `event_staff_users.sort_by{|u| u.display_rank or 99999}` worked and puts the nils last. using or 0 would put them first. – Dan Oct 26 '17 at 18:35
18

Just to expand on @Robbie's answer

Post.all.sort_by {|post| post.custom_method }.reverse
jwarchol
  • 1,906
  • 15
  • 12
  • Be careful if there are going to be many posts. Getting them all out of the DB and loaded up into ActiveRecord objects is not free. When the number of records becomes large, the slow down will matter for a production application. A field that's actually in your database, with a proper index, and limiting the query to a paginated set is the way to go. – jwarchol Oct 29 '10 at 21:31
  • Pagination, yes granted. But proper index is negligible because my custom method produces a ranking based on time. Which is always changing. Thus the results can change instantaneously. – Trip Oct 29 '10 at 21:44
  • I've worked a little with ranking systems. They can be tricky to do in real time for large sets of records when the calculation is not trivial. Best of luck, if you find any useful techniques, please share :-) – jwarchol Oct 29 '10 at 22:02
  • You could shorten this to ```Post.all.sort_by(&:custom_method).reverse``` – Adem Dinarević Feb 03 '20 at 12:47
15

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>
Sia
  • 8,894
  • 5
  • 31
  • 50
5

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

MZaragoza
  • 10,108
  • 9
  • 71
  • 116
3

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

Robbie
  • 715
  • 9
  • 19
1

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))
Greg
  • 55
  • 1
  • 7
-4

in rails 3 we can do this as: Post.order("custom_method DESC")
When upgrading app from rails2 to rails3

Rick Hoving
  • 3,585
  • 3
  • 29
  • 49