Rails 4.1 has recently been released and it came out with the usual bag of goodies, one of the most notable being ActiveRecord enums, a handy new feature that simplifies the creation and use of state attributes for your models.
Consider the following use case: our app needs users, and each of them has a state that can be registered
, active
or blocked
.
How would have we addressed the task in the past? Well, probably by adding a string or integer field to the users
table in order to hold the state value, and by writing a few scope methods to query the table. Nowadays it’s much simpler: you just need to write a migration that adds the field to the table:
class AddStatus < ActiveRecord::Migration
def change
add_column :users, :state, :integer
end
end
and add the enum macro to the User class:
class User
enum state: [:registered, :active, :blocked]
end
Let’s see in details what we’re getting. The user state is nil
by default; we can query each state label to see if it matches the current user’s state:
user = User.new
user.state
# => nil
user.registered?
# => false
user.state = :registered
user.registered?
# => true
Want to update the state and save the record in one command? Easy enough:
user.registered!
user.persisted?
# => true
user.registered?
# => true
We also get handy scope methods for each state:
User.active
# => #<ActiveRecord::Relation []>
User.registered
# => #<ActiveRecord::Relation [#<User id: 7, status: 0...]>
We can even create users with a specific state using the enum scopes:
User.registered.create
# => #<User id: 6, status: 1, ...>
Can we inspect the actual state column value, before typecast? Of course we can, but #state_before_type_cast
returns the enum label. So we’re going to use the []
method:
user.state
# => "registered"
user.state_before_type_cast
# => "registered"
user[:state]
# => 0
So, now we can fully understand how it works: ActiveRecord actually stores in the database the integer corresponding to the enum label position inside the array that we provided to the <code>enum</code> macro.
Do you want a default value? Then add a default value to the state field:
class ChangeStatus < ActiveRecord::Migration
def change
change_column :users, :status, :integer, default: 1
end
end
From now on all instantiated users will have a default state value corresponding to the position we provided in the migration:
user = User.new
user.state
# => "active"
Be aware there are a few reserved words that cannot be used as enum labels, most notably existing column names, existing class methods and a few more. If you happen to use one of them by mistake then the app will complain raising an error:
class User
enum state: [:logger]
end
# => ArgumentError: You tried to define an enum named "state" on the model "User", but this will generate a class method "logger", which is already defined by Active Record.
If you are interested in the actual Rails implementation you can peep at the code and tests on the github repository, of course. Have fun!
Leave a Reply