Stop storing strings when you should use Enums
A dive into one of the most interesting but surprisingly little known ActiveRecord feature.
A status problem
Let’s say you are building an e-commerce application in Rails. Of course, you will store orders
in your database. These orders
have different statuses
. They can be pending
(not paid yet), paid
or canceled
. How would you code this?
When I first faced this problem, I probably had the worst answer. I stored the status
of my orders
as strings. The result?
- First, my code was clunky. You don’t want to write
Order.where(status: "paid")
everytime you need a specific set of orders. So you write helpers… But you need a lot of them… So you give up. - Second, it was unreliable. What if, in one of your controller, you make a typo and write
order.update(status: "peding")
? Of course, you should catch this with your tests. But if not? Well, you are screwed and your data has just become distrustful.
There are several other ways to solve this problem. You could, for example store each status
in your database as a boolean. You would have a paid
column in your database that you would set to true or false. And the same with a pending
and a canceled
column. So ugly.
Today we gonna talk about what I think is the most efficient way to deal with this: ActiveRecord Enums.
Say hello to Rails magic
While a lot of people hate Rails magic, I love it. I love to be given ready-to-use, reliable and elegant solutions to solve my code problems. Enums is one of them. With Enums, you can map your attributes to integers in your database but query them by their real name. This can work for status
columns but also for various other use cases.
- Positions in a company (employee, manager, VP, etc.)
- Phone types (office, mobile, home, etc.)
- Gender (man or woman)
- etc.
The list goes on and on. You can basically use Enums every time your attribute takes its value among a predefined set of options. Of course, you can also expand these options later.
Enums gives you the best of both worlds: the readability of strings and the reliability of integers. To help you understand, let’s write some code.
Quick setup…
To be able to use Enums, we will first need a rails migration. As we said, we will only store integers in our database.
class AddStatusesToOrders < ActiveRecord::Migration
def change
add_column :orders, :status, :integer, default: 0
end
end
Great ! Now, we need to tell ActiveRecord which status
should correspond to each integer we store in our database. We do this in our model.
class Order < ActiveRecord::Base
enum status: [:pending, :paid, :canceled]
end
Please beware that the order of our array of symbols is really important here. It means that the order
is pending
if you store 0 as its status
in the database, paid
if you store 1, canceled
if you store 2. If you change the order, the mapping change.
To avoid any mistake, you can also precisely map the integer values to the statuses
like below.
class Order < ActiveRecord::Base
enum status: { paid: 1, pending: 0, canceled: 2 }
end
Unveiling the power of Enums
Now that we are done with the setup, we can begin to play with our Enum. First, go to your console and try to get the status
of an order
.
Instead of this:
irb(main):001:0> Order.first.status
# => 0
You should see this:
irb(main):002:0> Order.first.status
# => "pending"
So much easier to deal with for us as developers!
Enums also gives you access to a set of really useful methods and scopes. Let’s have a look!
- Check if an instance has a specific status
irb(main):003:0> Order.first.paid?
# => falseirb(main):004:0> Order.first.pending?
# => true
- Change the status of an instance
irb(main):005:0> Order.first.paid!
# => trueirb(main):006:0> Order.first.status
# => "paid"
- Getting an ActiveRecord_Relation of
orders
with a specificstatus
irb(main):007:0> Order.paid
Order Load (29.9ms) SELECT "orders".* FROM "orders" WHERE "orders"."status" = $1 [["status", 1]]
# => An ActiveRecord_Relation containing our paid orders
- Getting all the
statuses
of ourorders
(to use for a select field in a form for example)
irb(main):008:0> Order.statuses
# => { "pending" => 0, "paid" => 1, "canceled" => 2 }irb(main):009:0> Order.statuses.keys
# => ["pending", "paid", "canceled"]
Going Further
Enums will really make your life easier. To learn more, below is a list of interesting articles and resources:
- The ActiveRecord::Enum official documentation
- Creating Easy, Readable Attributes With ActiveRecord Enums
- When Enough Is Enough: How to Know When to Use Enums
- Multiple Enums with same values in Rails 5 (AKA adding prefixes and suffixes to Enums)
- The Enumerize Gem, which goes a bit further than raw Enums if you have specific needs
I let you with this tweet from DHH. Yes, I really love Rails magic!