25

I thought it would be good to populate a status field in an activeRecord table using constants. However, when it comes to checking if this status has a particular status, I'm having trouble.

If I do the following,

e = Mytable.new
e.status = :cancelled
e.save

then refind the record and try and compare my status to the symbol, the check fails. I have some output from the console to show this.

irb(main):060:0> e.status.eql?("cancelled")
=> true
irb(main):061:0> e.status.eql?(:cancelled)
=> false
irb(main):062:0> e.status == :cancelled
=> false
irb(main):063:0> e.status == "cancelled"
=> true
irb(main):064:0> e.status == :cancelled.to_s
=> true

Is there a better way of holding a status in a record? Is there a way of testing if a current field value is equal to the :symbol without converting the :symbol to a string? I'm thinking there may be an operator I'm not aware of.

seanyboy
  • 5,623
  • 7
  • 43
  • 56
  • 1
    ecoologic has a good solution for you, but I would recommend maybe steering away from this and maybe making a class with constants in it. That you can do things like `e.status = Statuses::CANCELLED` and what not. And internally that could be a string and it doesn't matter. You're still using constants, and it will error out if that constant doesn't exist, and it's cleaner that way. – MrDanA Jan 27 '12 at 18:24
  • why don't you override the getter of your column? – apneadiving Jan 27 '12 at 18:35
  • I amended my answer before reading these two comments but I'd like to say that I love @MrDanA solution, you should write an answer and I'll vote it! – ecoologic Jan 27 '12 at 21:09

7 Answers7

13

With Rails 4.1.0, you'd probably want to use Active Record enums.

To quote the official release notes:

class Conversation < ActiveRecord::Base
  enum status: [ :active, :archived ]
end
 
conversation.archived!
conversation.active? # => false
conversation.status  # => "archived"
 
Conversation.archived # => Relation for all archived Conversations
 
Conversation.statuses # => { "active" => 0, "archived" => 1 }
jiehanzheng
  • 993
  • 13
  • 12
9

This is kind of late, but may help someone else.

If you have classes with different statuses, you might consider an approach using constants along with scopes like this:

class Account < ActiveRecord::Base
  #-------------------------------------------------------------------------------
  # Configuration
  #-------------------------------------------------------------------------------

  # STATUS is used to denote what state the account is in.
  STATUS = { :active => 1, :suspended => 2, :closed => 3 }

  # Scopes
  scope :active, where(:status => Account::STATUS[:active])
  scope :suspended, where(:status => Account::STATUS[:suspended])
  scope :closed, where(:status => Account::STATUS[:closed])
  ...
end

Then you can easily find records based on status, like so:

# get all active accounts
active_accounts = Consumer.active.all
# get 50 suspended accounts
suspended_accounts = Consumer.suspended.limit(50)
# get accounts that are closed and [some search criteria]
closed_accounts = Consumer.closed.where([some search criteria])

Hope this helps someone else!

EDIT: If you're more into using gems, the simple_enum gem looks like an excellent alternative.

MikeH
  • 796
  • 7
  • 18
7

As of Rails 4.1, Active Record now supports enums

From the release notes:

2.5 Active Record enums

Declare an enum attribute where the values map to integers in the database, but can be queried by name.

class Conversation < ActiveRecord::Base
  enum status: [ :active, :archived ]
end

conversation.archived!
conversation.active? # => false
conversation.status  # => "archived"

Conversation.archived # => Relation for all archived Conversations

Conversation.statuses # => { "active" => 0, "archived" => 1 }

Additional documentation here: http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html

readonly
  • 343,444
  • 107
  • 203
  • 205
7

If I remember well symbols in ActiveRecord are stored in yaml format, some kind of conversion must be done because there is no such thing as a symbol in relational db (which I'm aware of, at least). When you read it it's then a string which will not match your symbol and not even the string of the symbol, in fact it should be something like:

:x # => "--- :x\n"

I think this plugin can solve your problem, but I haven't used it honestly. https://github.com/zargony/activerecord_symbolize

* EDIT *

I leave the above because I remember that was the situation I had and I can be corrected if I'm wrong, nonetheless I'm trying this right now and the stored value (Rails 3.1.3) is a simple string with the value of the symbol, so the following should be enough.

class Example < ActiveRecord::Base

  def aaa
    super.to_sym
  end

  def aaa=(value)
    super(value.to_sym)
    aaa
  end

end

This of course will force the value to always be a symbol

** EDIT AFTER AGES ** I think it's fine in this situation as it's clear that in the db it's a string and the logic is simple, but I strongly discourage overriding db attribute methods to add more complex logic.

ecoologic
  • 10,202
  • 3
  • 62
  • 66
  • 1
    also keep in mind that using :to_sym on user input is a bad idea: http://www.tricksonrails.com/2010/06/avoid-memory-leaks-in-ruby-rails-code-and-protect-against-denial-of-service/ – oseiskar Apr 25 '13 at 13:02
  • yeap, worth to mention, it's bad practise – ecoologic Apr 25 '13 at 22:52
7

At the request of ecoologic, here is my comment as an answer:

ecoologic has a good solution for you, but I would recommend steering away from this and making a class with constants in it. That you can do things like e.status = Statuses::CANCELLED. And internally that could be a string and it doesn't matter. You're still using constants, and it will error out if that constant doesn't exist, and it's cleaner that way.

Joel Brewer
  • 1,622
  • 19
  • 30
MrDanA
  • 11,489
  • 2
  • 36
  • 47
  • As a matter of best practice, where would you put this class? In it's own file in lib, or helpers? – seanyboy Jan 28 '12 at 11:21
  • I've place the status class in the model file. It's working well. I also added a self.method to the class to convert the status to a color. So that keeps all that in one place. – seanyboy Jan 28 '12 at 11:58
  • 1
    I usually put them in their own file in lib, yes. You can then automatically load files (instead of having to `require` them) by editing your application config file. – MrDanA Jan 28 '12 at 15:30
  • 1
    This answer is outdated. @jiehanzheng has an updated answer for Rails 4, using an enum. – TheJKFever Aug 01 '15 at 17:43
6

Also you can overwrite the reader method:

def status
  read_attribute(:status).to_sym
end
fguillen
  • 36,125
  • 23
  • 149
  • 210
1

From Programming Ruby 1.9, regarding the == operator in the Symbol class (p. 729):

Returns true only if sym and obj are symbols with the same object_id.

Whatever you have stored in the DB will always have different object_id than the fixed object_id of the symbol (a pointer to a string literal, in this case).

jordanpg
  • 6,386
  • 4
  • 46
  • 70