In Rails, how should I implement a Status field for a Tasks app - integer or enum?
Asked Answered
M

6

32

For a Rails 3.0 Todo app, I have a Tasks model with a Status field. What's the best way to store the Status field data (field type) and still display a human-readable version in a view (HTML table)? Status can be:

0 = Normal
1 = Active
2 = Completed

Right now I have this:

Rails Schema Here:

create_table "tasks", :force => true do |t|
t.integer "status", :limit => 1, :default => 0, :null => false

Rails Model Here:

class Task < ActiveRecord::Base
  validates_inclusion_of :status, :in => 0..2,
    :message => "{{value}} must be 0, 1, or 2"

Rails View Here:

<h1>Listing tasks</h1>

<table>
  <tr>
    <th>Status</th>
    <th>Name</th>
    <th></th>
    <th></th>
    <th></th>
  </tr>

<% @tasks.each do |task| %>
  <tr>
    <td><%= task.status %></td>
    <td><%= task.name %></td>
    <td><%= link_to 'Show', task %></td>
    <td><%= link_to 'Edit', edit_task_path(task) %></td>
    <td><%= link_to 'Delete', task, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>
</table>

Requirements

  1. Store a Task's status in the db such that the values are easily localizable, i.e. I'm not sure I want to store "normal", "active", "completed" as a string field.

  2. Solution must work with Rails 3.0.

Questions:

  1. Should I store the field as an integer (see above)? If so, how do I display the correct human readable status in an HTML table in my Rails view, e.g. show "Active" instead of "1" in the HTML table.

  2. Should I use an enum? If so, is this easy to localize later?

  3. Should I use straight strings, e.g. "Normal", "Active", "Completed"

  4. Can you provide a quick code sample of the view helper, controller or view code to make this work?

Metaprotein answered 16/4, 2010 at 5:49 Comment(1)
You're asking quite a bit in this question. You'd probably get better answers by splitting it into multiple separate questions.Pru
A
43

1.It depends on how much you want to optimize queries on the DB.

2.Not really, it is not supported 'out of the box' by AR. # As of Rails 4 enums are supported out of the box.

3.IMHO you can use strings without a big performance penalty (just remember to add field to an index). I would do this because it's easier to internationalize and to maintain. However, you can go with integers if you need extra performance.

You may take a look on 2 SO threads here and here where this is debated.

4.If you want to keep them as integer, here is how you can accomplish this:

class Task << AR::Base
  NORMAL    = 1
  ACTIVE    = 2
  COMPLETED = 3


  STATUSES = {
    NORMAL    => 'normal',
    ACTIVE    => 'active',
    COMPLETED => 'completed'
  }

  validates_inclusion_of :status, :in => STATUSES.keys,
      :message => "{{value}} must be in #{STATUSES.values.join ','}"

  # just a helper method for the view
  def status_name
    STATUSES[status]
  end
end

and in view:

<td><%= task.status_name %></td>

If you want to use strings, it's more simplified:

STATUSES = ['normal', 'active', 'completed']
validates_inclusion_of :status, :in => STATUSES,
          :message => "{{value}} must be in #{STATUSES.join ','}"
Accroach answered 16/4, 2010 at 7:17 Comment(1)
Thanks for your thorough answer. You've convinced me to stick with strings for the simplicity.Metaprotein
P
6

The easiest thing to do would be to just store the actual strings in the field instead of adding another table. Depending on your point of view this is either a bad idea as your database will not be sufficiently normalized or a great idea as your app is now more efficient for being normalized.

If you opt to not do that and to keep the values in a separate table; you need to setup the relationships in the model.

class Task < ActiveRecord::Base
    has_one :status
end

class Status < ActiveRecord::Base
    belongs_to :tasks
end 

Then in your view you can reference the value by:

<%= task.status %>
Pru answered 16/4, 2010 at 7:21 Comment(1)
It should be the other wayCerellia
B
3

I have used Enum-Column for such use cases. The plugin allows you to define a enum column type in your migration script and creates a MYSQL enum column type for the attribute.

create_table :tasks do |t|
  ...
  t.enum :status, :limit => [:normal, :active, :completed], :default => :normal
  ...
end

Now in your code you can do the following:

task.status = "active"
task.status = :completed
p "Task status: #{task.status}" # prints Task status: completed


Task.find_by_status(:active)
Task.find_by_status("active")
Task.find_by_status(2)

Works well with serialization too:

task.to_xml # will result in status= active instead of status-2

Other nice aspect is, the status values are displayed as strings when viewed using a regular DB client(E.g: mysql command console or phpMyAdmin)

The plugin provides optimal storage and user friendly access for the enumeration types.

Caveat:

The plugin is quite old and not maintained. I am using it extensively with MySQL DB. I had to patch the code to get it work on PostgreSQL. If you are using MySQL, this plugin is a good choice.

Beauvoir answered 16/4, 2010 at 7:36 Comment(0)
E
2

I prefer to store "normal", "active", .. "completed" as string in the table because:

  • it's self documentation (for example, someone may look at the data but never read the Rails source code)
  • there is little (if not no) performance penalty
  • it is (still) easy to do i18n by means of Rails virtual attribute in the model or whatever layer in other languages

These days, I tend to decouple Rails constants from the database as much as I can. There are always some PHP/MSSQL/??DBA folks around us (who may not love Rails as much as we do ;-)

So, the answer is not integer nor enum (but a varchar ;-)

Echinoderm answered 16/4, 2010 at 7:37 Comment(1)
I cam to regret this decision, when we needed to rename a few status due to business reasons. 10 million records in production to update vs 1 line of code if I used enums or the route suggested in the accepted answerAinsworth
A
1

I know this is an old question but I wanted to mention 2 points that come from experience, especially if someone is looking for this now ( 2014 - OQ was in 2010) :

  1. If you are starting a new project > Rails 4 ( technically ActiveRecord 4) - use Enums - the most efficient route. Especially if you need to create any kind of complicated SQL queries later on.
  2. There is on more alternative - create a composite Status model that will hold statuses for all your other models. Make it an STI model (add type column)- then you can create things like OrderStatus < Status or CustomerStatus < Status and your Order and Customer would have status_id attributes. This makes for slower (!) and more cumbersome (!) queries, however you might want to go this route if you are a creating an app that will be shipped to client that has no technical expertise and they would need some kind of ability to add / remove statuses through something like ActiveAdmin on their own rather than modify your code base. This is also an option if your data layer can't handle enums.
Ainsworth answered 3/10, 2014 at 20:38 Comment(1)
Rails enums are really complicated and confusing. See this article for some gotchas: hackhands.com/ruby-on-enums-queries-and-rails-4-1Persia
R
0

Using integer to store the status is a good idea. Nonetheless, I think the code provided by the accepted answer is not elegant since it pollutes the whole model class just because of an attribute.

My suggestion is overriding the attribute getter:

def status
  {
    0 => "active",
    1 => "inactive"
  }[read_attribute(:status)] # use the read_attribute method to prevent infinite loop.
end

The logic of transforming the integer into a string will be only in this getter method, so you don't need to make the class dirty.

Recurved answered 26/11, 2014 at 7:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.