Words and Code

One writer’s journey from words to code.

When Enough Is Enough: How to Know When to Use Enums

#technicaltuesdays, rails

NERD ALERT: I love databases. God, they’re just fantastic. Few things give me as much joy as an empty whiteboard and a couple of intricate join tables that need to be sketched out. But there’s also an art to understanding how to handle data – particularly when you have a shit ton of it.

The main issue people run into with data is first, how to go about storing it, and second, how to go about getting it when you actually need it! This doesn’t sound super complicated though, right? Wrong. Databases grow vertically, not horizontally, and they grow hella fast. At a certain point, the way that you go about storing your data ends up directly impacting how you go about retrieving it!

Many Rubyists write different helper methods to do their querying for them. But this is Rails Land, which means that we have black Rails magic at our disposal. And with the advent of Rails 4.1, that magic now has a name: enum.

Get Yo Enum On

When Rails 4.1 was released less than a year ago, it came out with a bunch of new features. One of those was ActiveRecord enums, which essentially cut out a lot of methods and superfluous code.

ActiveRecord enums allow you to manipulate the attributes of an ActiveRecord object in Rails such that an attribute’s values map to integers in the database (as opposed to strings), and yet can also be queried by name. If all this sounds kind of crazy to you, that’s because it is! Enums are pretty cool because they have the flexibility of a string, but the speed and efficiency of an integer. You can look up values in a huge database using an integer, but you can also update and add attributes as string. Oh – you also get a bunch of methods for FREE!


So how does this magic work, exactly? I thought you’d never ask!

Multiple Columns?! Ain’t Nobody Got Time For That!

To demonstrate how to implement ActiveRecord enums, I’ll continue using last week’s ecommerce bookstore app as my example. So, I have these Book objects in my store, and I want to keep track of what their status is: either in stock, out of stock, or ordered. For the sake of simplicity, we’ll pretend that I’m not a super sophisticated bookseller, so I don’t have a lot of Books, and therefore each Book object can only ever have one of these three states.

I might first start off by having three different columns to represent each of these states:

1
2
3
4
5
6
7
class AddStatusesToBooks < ActiveRecord::Migration
  def change
    add_column :books, :in_stock?, :boolean
    add_column :books, :out_of_stock?, :boolean
    add_column :books, :ordered?, :boolean
  end
end

But this seems kind of…ridiculous. I’m always going to have two empty columns. This code has me all like:


Alternatively, I could combine all of these columns into one and use some helper methods that return boolean values when I call them on an instance of a Book:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Book < ActiveRecord::Base
  def in_stock?
    self.status == 'in stock'
  end

  def out_of_stock?
    self.status == 'out of stock'
  end

  def ordered?
    self.status == 'ordered'
  end
end

Well, now I have one column, so that’s better. But this is still so repetitive, ugly, and inefficient. Don’t worry, it’s about to get real good.

Query Your Little Heart Out

Implementing enum is pretty simple. First, add the macro to your class:

1
2
3
class Book < ActiveRecord::Base
  enum status: [:in_stock, :out_of_stock, :ordered]
end

You’ll notice that I have my status attribute categorized into my three different options. The fact that they look like symbols in an array is no mistake – each of these symbols is actually associated with its index. So in_stock will be referenced by its index number 0, out_of_stock by 1, and ordered by 2.

Next, add a column to your migration that will allow ActiveRecord enum to carry out your queries:

1
2
3
4
5
class AddStatusToBooks < ActiveRecord::Migration
  def change
    add_column :books, :status, :integer, default: 0
  end
end

I want all my Book objects to default to an in_stock status when they are created, so I added a default: 0 to my column. Adding a default is always a good way to keep your code defensive.

Now if I want to create a Book object and check its status:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
book = Book.create

book.status
# => "in_stock"

book.out_of_stock?
# => false

book.status = :ordered
book.ordered! #updates the object
book.ordered?
# => true
book.status?
# => "ordered"

book.out_of_stock
# => #<ActiveRecord::Relation []>

Go ahead and scroll up if you don’t believe me – I promise I didn’t make any of these methods! Enum gave them to me, FO FREE. So what’s actually going on here? Well, not much more than ActiveRecord mapping the integer corresponding to the indexes of the symbols we provided in the enum macro array.

Our database has only one column storing all this information: status. That column has rows that are all either 1, 2, or 3. ActiveRecord pulls the actual symbols that correspond to these array indexes, and returns them, simultaneously creating an in_stock, out_of_stock, and ordered scope. And the icing on the cake: all the helper methods that are immediately generated for us in the process!

Tips and Tricks

Enums give you a lot of flexibility. For example, I played around with the in_stock scope of my Book object to create a specific class method that would order the top three newest books added to my inventory:

1
scope :newest_stock, -> (limit: 3) { in_stock.order('date DESC').limit(limit) }

Another thing to remember is that you cannot use the same names for different enums of the same class:

1
2
3
4
class Book < ActiveRecord::Base
  enum status: [ :ordered ]
  enum inventory: [ :ordered ]
end

Definitely don’t do this – this will raise an ActiveRecord error!

A huge upside to using enums is their contribution to your application’s speed and performance. It’s a well-known fact that it’s much cheaper to store data as an integer in memory, rather than as a string value. Enums take advantage of that, yet allow you to use all the ActiveRecord methods you know and love.

So, you can have your human-readable and fun-to-program code without sacrificing any of the speed and performance that you need to save and access your information. I guess dreams really do come true.

tl;dr?

  • The ActiveRecord enum feature allows you compose a single complex state on your models, and can help avoid using multiple boolean value columns to check the status of different objects.
  • There are a bunch more resources out there, so make sure you check them all out.
  • Check out the enum release notes and documentation.