a new feature of Ruby on Rails 4.1 is that of “enum” which enables a simple, and very quick, mini state machine.
The use case for such a feature is for a status field that has a small set of values.
Back in r2 I would have used a lookup table approach with a separate status table with id and label columns. This had the (seeming) advantage of being easy to update and add to. The reality was it very rarely needed to be updated (and indeed, hardly ever should be). The disadvantages being the need to seed the database with initial values any methods dependted on known ids (i.e. status.id = 1 being status.label = “Draft” ) and all the joining with major tables that needed to happen. Hence my database was full of tables like “user_status”, “post_status”, “invoice_status”, etc). This practice came out of my background of heavy relational design where the database base is the canonical keeper of all data big and small. (Note that is is entirely appropriate is you have a long list, more the 20?, and especially if you have more data than just a id and a label name)
Sometime in the R3 stream, I moved to preferring Constant array of Strings, and storing the status in the db as a :string, with a limit: 10 . With the use of some custom scopes and validates_inclusion_of work I was very happy with this approach. (and I had recently started to store just the array position in the database )
1 2 3 4 5 6 7 8 9 | class Page < ActiveRecord::Base ... STATUSES = ['draft', 'pending', 'approved', 'published', 'archived'] .... def pending_status self.status = 'pending' end .... end |
(This still seems reasonable if you have a medium list but not long enough to be worth stuffing in a table. So more than 10 but less than 20, with the possibilities of a future refactoring it a another class if it need to be shared, or a lookup table if it grows. )
Rails 4 use of “enum” looks to get me the benefits of my hash constant approach with more functionality and less work!
1 2 3 4 5 | class Page < ActiveRecord::Base ... enum status: [ :draft, :published, :archived ] .. end |
alternately, you could also define in ruby 2 the key/values symbol array with a :
1 | %i(draft published archived) |
If you needed to explicitly set the key values (legacy issue, the need to remove a unused state, or some programmatic cleverness?) you can also use a hash
1 | enum clever_status: [ draft: -1, published: 1, archived: 99 ] |
(Note : the implicit values are 0, 1, etc… as soon you start reordering or adding states, especially if you have a app in production, you will want/need to move to explicit key values)
you will naturally need to add a integer column to the table. By setting an initial value for status column you set a initial state for the status (whatever makes sense for your application).
1 2 3 4 5 6 | class AddStatusToPages < ActiveRecord::Migration def change add_column :pages, :status, :integer, default: 0 add_index :pages, :status end end |
I’ve also added an index on the status column since it is acting as a foreign key and will be queried on. (best practice)
If you are using PostgreSQL you could increasing performance using a feature called “Partial Indexes” (since version 7.2), basically an index using a WHERE clause, in this case mapping it to that value-states of the status column.
1 2 3 | add_index :pages, :status, where: "status = 0" add_index :pages, :status, where: "status = 1" add_index :pages, :status, where: "status = 2" |
In SQL Server, this type of index is called a filtered index. see Partial Indexes: Indexing Selected Rows for more. Also despite its name, the AR enum does not use the ENUM type that is implemented in some databases.
One of the advantages of the Rails 4 ActiveRecord enum feature is that it automatically creates both predicate and bang helper methods for each status value :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | page.draft! page.draft? # => true page.published? # => false page.status # => "draft" page.published! page.published? # => true page.draft? # => false page.status # => "published" page.archived! page.archived? # => true page.published? # => false page.status # => "archived" page.status = nil page.status.nil? # => true page.status # => nil |
This saves the work of manually creating these methods on your own.
You also get ActiveRecord scopes defined for your status values.
1 2 | Page.draft #=> a scope to find all draft pages Page.published #=> a scope to find all published pages |
a major caveat in using ActiveRecord enum feature is if you have several enum in the same class, avoid using the same state values ( :draft, :published, :archived ), also you can get conflicts with existing method names.
Given that the methods used those state values are used to create the helper methods, I find that using lower case makes the most sense, but in order to have good values for the users to select in the views I use the rails titleize to make better view display drop downs.
1 2 3 4 | < %= f.select :status, options_for_select(Page.statuses.collect { |s| [s[0].titleize, s[0]] }, selected: @page.status), {} %> |
If you need to work around the caveats then falling back to the Constant array of Strings approach and create the methods and scopes you need (and with names that don’t conflict)
There also exist gems for full blown Finite State Machines (FSMs) if you need to go there, usually after creating methods to validate certain states gets messy (don’t change state to “approved’ unless state was pending and user with the manager role toggled the pointy_head button). There are also numerous gem to add enum functionality that existed prior to Rails 4.1 and add it to older versions.
All in all the ActiveRecord enum feature simplifies the work needed to add simple state machines to your models and is a win!
Aug 23 update : I’ve come across a gem : Help ActiveRecord::Enum feature to work fine with I18n and simple_form which looks to simplfiy working with the Simple_form gem, with worked fine for me via code like this :
1 2 3 | < %= f.input :status, collection: Page.statuses.collect { |s| [s[0].titleize, s[0]] } %> |
but the helper gem making it easy to use is okay too.
Also the use of I18n was something I was pondering in order to allow more expressive state values to the end user, but have also realized that it should allow a way to stop name value conflicts by having the short and pithy descriptions in the I18n yml but longer one in my code i.e.
1 2 | enum status: [ :draft_status, :published_status, :archived_status ] enum type: [ :draft_type, :published_type, :archived_type ] |
with the corresponding helper methods unlikely to conflict with each other or other method names.