简体   繁体   中英

Rails: Custom ordering of records

Does ruby or rails provide a method to order strings in a specified order? Say that I have the following priorities "Severe, High, Medium, Low".

These priorities are not going to change frequently (if at all). I have a Task model with a priority column:

tasks
  - id (integer)
  - name (string)
  - priority (string)

I'd like to get an array of all the tasks ordered by priority. Since the logical order does not follow alphabetical order, it's not possible to simply order by the priority column:

Task.all(:order => :priority)

What I've done is create a Priority model and defined the associations: Task belongs_to Priority. In the priorities table, I then assigned each priority name a value and ordered by that value. Is there a better way to do this? I'd rather not have a priorities table at all and declare a PRIORITY constant (as a hash), or simply specify the priority as a string in the tasks table.

You could use a case statement in your where clause. It's a little ugly but should work:

class Task
  PRIORITIES_ORDERED = ['high', 'medium', 'low']

  # Returns a case statement for ordering by a particular set of strings
  # Note that the SQL is built by hand and therefore injection is possible,
  # however since we're declaring the priorities in a constant above it's
  # safe.
  def self.order_by_case
    ret = "CASE"
    PRIORITIES_ORDERED.each_with_index do |p, i|
      ret << " WHEN priority = '#{p}' THEN #{i}"
    end
    ret << " END"
  end

  named_scope :by_priority, :order => order_by_case

end

And then when you want them ordered by priority, you can do something like this:

Task.all.by_priority

Of course as other people have mentioned, it's cleaner to make a foreign key to a Priority table instead of a text string column. In the Priorty table, you could add a position column as an integer that you could sort by, and plug-ins exist to make this easier.

Here's up updated version that works for rails 4.1 and a little customized:

  PRIORITIES_ORDERED = ['Unknown', 'Critical', 'Warning', 'Ok']
  def self.order_by_case
    ret = "CASE"
    PRIORITIES_ORDERED.each_with_index do |p, i|
      ret << " WHEN status = '#{p}' THEN #{i}"
    end
    ret << " END"
  end

  scope :order_by_priority, -> { order(order_by_case) }

I would use integers in db. It's easier to sort and index, then override the attribute getter and setter to use symbols to interface externally.

Switch to a integer like Aaron said, then you will probably want to use a default_scope .

For example:

class Task < ActiveRecord::Base
  default_scope :order => 'tasks.position' # assuming the column name is position
  ...

With a default scope you won't have to specify the sort order in any of your find method calls - the tasks will automatically be sorted.

If changing your schema to use an integer priority isn't an option, you can also define a custom <=> method on Task to define the sort order.

class Task

  PRIORITIES = {
    :high => 3,
    :med  => 2,
    :low  => 1,
  }

  def <=> (other)
    PRIORITIES[self.priority] <=> PRIORITIES[other.priority]
  end

end

The downside of doing it this way is that you'll need to do the sorting on the Ruby side, rather than let your database do it for you, which will preclude the use of default_scope . The upside is that any time you call sort on an Array of Tasks the order will come out correctly (or, if you only want a custom ordering in one particular place, you can use sort_by ).

Just to solve your specific question: You can order it by the last letter.

Sever(e), Hig(h), Mediu(m), Lo(w)

you can either do it in rails or sql:

order by RIGHT(priority, 1)

I choose this approach because

  1. it is the easiest possible way.
  2. you mentioned it is not going to change.

the approach might not be flexible enough, but too me, I try not to generalize the problem unless it is necessary

I would suggest using acts_as_list ( http://github.com/rails/acts_as_list/tree/master ), which will add a position field to your object but will also give you all the methods for moving things up and down the list while preserving the order correctly.

If the priorities are never going to change it might be better to make priority a field on the task table. The problem is that you then need to be able to sort by priority. For that you have a few options, but whatever you choose it should be done by the db.

Here's an updated version for Rails 7:

Task.in_order_of(:priority, %w[Severe High Medium Low])

Docs and PR + discussion which introduced it

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM