Girders Blog
about building the internet

Field Guide to the Rails ActiveRecord `enum`

The Rails enum allows you to use nmemonic names instead of integer values for a table attribute.

Historical Note: The term enum comes from the C family of languages that does the same thing. It is not related to the Ruby Enumerable mixin.

Assume we have an Account model with an integer status attribute from the table. Each value corresponds to a given state of the record. Declaring an enum on status allows the code to use a name of the state instead of the actual integer value.

The simple use case involves a sequential set of integers starting from zero.

enum status: [:signup, :active, :closed]

This maps values from the table to the model:

0 => "signup"
1 => "active"
2 => "closed"

Each value creates a set of methods on the model to allow you to use the names instead of the integer values.

We can see what the mapping is with this class method:

Account.statuses    #=> {"signup"=>0, "active"=>1, "closed"=>2}
Account.statuses[:active]  #=> 1
Account.statuses["active"] #=> 1 (Hash with indifferent access)
Account.statuses.key(1)    #=> "active"

Scopes (class methods) are created for each named value in the mapping:

Account.<name>
Account.not_<name>

Also, it creates instance methods for each name in the mapping:

account.<name>?    # test for the value
account.<name>!    # update the record to that value

Let’s dive into the specifics.

Getting and Setting Values

NOTE: For brevity, I will concentrate on the “active” name and it’s uses. The “signup” and “closed” names work the same way.

From the account instance, we can get the value of the status as a string. ActiveRecord automatically converts the number into the string name for us. If you need to get access to the original integer value, use the class statuses method or the <attribute>_before_type_cast method.

account.status                   #=> "active"
Account.statuses[account.status] #=> 1
account.status_before_type_cast  #=> 1

We can set the value using a string, symbol or integer, or nil. Attempting to set an invalid value will raise and error. This will not trigger a save to the database.

account.status = "active"  # Change the value by name as string
account.status = :active   # Also takes the symbol
account.status = 1         # and also by the integer value
account.status = nil       # We can also set it to nothing
account.status = 99        #=> ArgumentError: '99' is not a valid status
account.attributes = {status: :active} # set from a hash

Updating

Update the record with the <name>! method such as (active!). The update! method can also take a hash with the status value as a string, symbol, integer, or nil. Again, it will raise an error if you pass in invalid value.

account.active!            # Updates with validations like below
account.update!(status: "active", status_at: Time.now)
account.update!(status: :active, status_at: Time.now)
account.update!(status: 1, status_at: Time.now)
account.update!(status: nil)

Conditions

To check the value of the status, use a <name>? method such as active?. While you can check against the string value, it may stop working if the enum is updated. Using the method will raise an exception if the definition changes, which will be easier to find and fix.

account.active?            #=> true, same as account.status == "active"
account.status == "active" # true, but use active? instead
account.status.nil?        # Checking for nil
account.status == :active  # false --> This does not work!
account.status == 1        # false --> This does not work!

Scopes

The enum also creates scope query methods for each named value, such as:

Account.active             #=> Account.where(status: 1)
Account.not_active         #=> Account.where("status != ?", 1)

You can disable scope creation with the _scopes option:

enum status: [:signup, :active, :closed], _scopes: false

The mapping also works when you specify the status name in the hash format of where clauses. It does not work with string expressions, nor does it raise an error if you use an invalid value. Invalid values may result in type mismatches in the generated SQL and throw a database exception.

Account.where(status: "active")
Account.where(status: :active)
Account.where(status: [:signup, "active"]) # Use the "status in set" with values
Account.where(status: :badvalue) # SQL is: "where status = 'badvalue'" which will fail
Account.where("status = ?", "active") # Fails similarly
Account.where("status = ?", Account.statuses[:active]) # This works

Default Value

Specify a default value with the _default option:

enum status: [:signup, :active, :closed], _default: "signup"

Name Collisions and Prefixes

Since each mnemonic name creates instance methods, you can have naming collisions between two enum declarations. Also, the names shouldn’t match any ActiveRecord or other defined instance method. Rails will throw an error if you have naming collisions. In this case, try another word (like a synonym) or use the _prefix or _suffix options to prefix/suffix the method names.

enum status: [:signup, :active, :closed], _suffix: true
enum billing_status: [:none, :active, :expired], _prefix: :billing

You only may need to adjust one of the declarations, not both. This creates the following family of methods for the “active” name:

account.active_status?     # <name>_<attribute>?  (suffix format)
account.billing_active?    # <prefix>_<name>?     (prefix format)

Using Non-Sequential Numbers

By specifying a hash of name/value pairs, we can override the starting point (zero) and match existing table data.

enum status: {signup: 10, active: 20, closed: 30}

We left “space” between these values so whenever new states or sub-states are introduced, they could be inserted between these values. Imagine if we need a new “suspended” state and it fits between the active and closed state in the account life-cycle. This is easy to insert:

enum status: {signup: 10, active: 20, suspended: 25, closed: 30}

Also, this technique allows us to use “ranges” in well-sorted numerations.

Non-Numeric Values

Even if our status column was a text column, we could still use the enum to control it. Remember, the format is {internal_name: "database value"},

enum status: {signup: "new", active: "active", closed: "closed"}

Here, we demonstrate mapping the “new” value in the table to the “signup” name in our application, which helps avoid the conflict with the “new” method in the class as well as gives it a better name. The active and closed values are unchanged, but we now have conditional methods and scopes for this column.

account.signup?
account.active!
Account.closed
account.status = "inactive" # Raises and error on an invalid value

This technique allows us to use PostgreSQL Enumerated Types with ActiveRecord as well.

Multiple names for a value

While you likely wouldn’t intend to do so, you could give the same value to multiple names:

enum status: {signup: 0, active: 1, closed: 2, delinquent: 2}

Consider this an alias or use to document a legacy name used in other applications. Both names can be used interchangeably, but Rails will only return one of them, and you can’t be sure which one. It may change if the enum pairs are reordered. If you do this, never check or pass the string value around, but any other use will still work.

account.status             #=> "closed" or "delinquent"?
account.closed?            # Good
Account.active             # Good
account.status == "closed" # Bad. Use account.closed?
variable = account.status  # Bad. The returned name string could change
other_account.status = account.status # Good

References