7

I've an array priority = ['HIGH', 'MEDIUM', 'LOW'] which is used to set an 'urgency' database column. I'd like to retrieve the data sorted by priority, though applying Task.order(:urgency) returns the results alphabetically (i.e. HIGH, LOW, MEDIUM).

I'm using PostgreSQL for the database.

I'd (obviously) like these to return from high to low priority. Is there a simple way to implement this, perhaps using the values' positions in the array?

3
  • 1
    what DB system are you using? MySQL ? PostgreSQL ? Something else? Commented Apr 21, 2015 at 14:47
  • PostgreSQL - sorry, should have mentioned. Will update now! Commented Apr 21, 2015 at 14:48
  • 2
    This doesn't directly answer your ordering question, but for "urgency" or "status", if you are using Rails 4.1+, consider using Active Record enums (Release Notes and Documentation). The pros are that it gives you scopes for free, and you can put your "urgency" in the order you need (the actual db column is an integer) - you can order based on that, which should perform a lot faster than a SQL CASE statement. I can provide a full example if wanted. Commented Apr 21, 2015 at 15:48

4 Answers 4

13

A simple CASE WHEN can do the trick (postgreSQL syntax used here):

scope :urgency_ordered {
  order(<<-SQL)
    CASE tasks.urgency 
    WHEN 'HIGH' THEN 'a' 
    WHEN 'MEDIUM' THEN 'b' 
    WHEN 'LOW' THEN 'c' 
    ELSE 'z' 
    END ASC, 
    id ASC
  SQL
}

Call it this way:

Task.urgency_ordered
Sign up to request clarification or add additional context in comments.

6 Comments

Works a charm - thanks MrYoshiji! I've had to wrap my scope in a lambda, as such scope :urgency_ordered, ->{order("CASE tasks.urgency WHEN 'HIGH' THEN 'a' WHEN 'MEDIUM' THEN 'b' WHEN 'LOW' THEN 'c' ELSE 'z' END ASC, id ASC")} (in case anyone else needs to use it the same way), but it does the job perfectly!
You don't need to use a lambda : a lambda for a scope is only necessary when you need dynamic values in your scope, like Date.today or a user argument (because the result of the scope depends on a variable). Here, in your case, the order clause does not depend on a variable, it is always the same way that the Task will be ordered.
Ah, makes a lot of sense. The reason I made the change was that I get a The scope body needs to be callable. error without it, and a little googling told me the lambda would fix it. It did in this case - though your rationale makes a lot of sense to me. For my own curiosity's sake, do you know why this might be? (Not to worry if not, my problem is solved after all!)
Are you using Rails 4 ? It might have been added that the scopes need to be callable, I am not really sure about it ...
Here is a similar (even better IMO) solution that allows you to generate code from a simple array of orderings: stackoverflow.com/a/37599787/1533892
|
4

I am late to the party, but I am surprised none has come up with this answer yet: If you are using MySQL (untested but should work for Postgres, too), there is a shorter, safer and more generic way than any of the previous answers. It works with any field and is protected against sql injection, so you can pass any list of values from the user.

Add the following scope to your model or ApplicationRecord:

class Task < ActiveRecord::Base
  scope :order_by_field, ->(field, values) {
    order(sanitize_sql_for_order(["field(#{field}, ?)", values]))
  }
end

You can now call the scope directly on your relation:

tasks.ordered_by_field(:priority, ["high", "medium", "low"])

Comments

3

If using Rails 4.1+, consider using Active Record enums (Release Notes and Documentation) for a DB agnostic solution:

class Task < ActiveRecord::Base
  enum priority: [ :high, :medium, :low ] 
  # Or enum priority: { high: 0, medium: 1, low: 2 }

  scope :priority_order, ->{ order(:priority) }
  scope :reverse_priority_order, ->{ order(priority: :desc) }
end

This will require a new integer column for the priority, and copying/moving the text data that you have to the new integer column, which could be done in a migration. then calling Task.priority_order should do the trick.

2 Comments

That looks a great solution. If I could mark two answers... Thanks for the input, I'll definitely be keeping this in mind in future!
Np, MrYoshiji's answer is a good one - it should work with any version of Rails, and doesn't require changing other parts of your model code and/or views. If you are lucky enough to be working with 4.1+, I like working with the enum feature. If you do decide to go this route in the future, I could post a migration - I would just need some more details about your existing code (to not break existing functionality).
2

It is late but here my 2cents. (Rails 4.2.1)

If you use the solution directly like this:

class Task < ActiveRecord::Base
    enum priority: { high: 0, medium: 1, low: 2 }
    scope :priority_order, order(:priority)
end

You will get this error: The scope body needs to be callable.

You need to call the scope like this.

scope :priority_order, -> {
    order(:priority)
}

define it like Proc or Lambda.

Why, you may ask :)

It makes sure what you have in the block is used each time the scope is triggered.

1 Comment

Good catch, if I'm suggesting a Rails 4.1+ feature, I guess I should supply Rails 4.0+ compatible scopes!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.