Design Rails enums the right way
Enum is a shortcut for the enumerated type, a data type consisting of a set of values. Rails provides enums in models, and the definition is straightforward:
class User < ApplicationRecord
enum status: { invited: 0, active: 1 }
end
However, while it’s very easy to define enum and use all the dynamic methods provided by Rails, there are some things we should be aware of not to create data integrity problems and code that is hard to maintain.
This article is a set of good practices for enums in Rails. If you want to read more about using enums, check the latest article. You can also do a deep dive and see how enums work under the hood.
The right enum definition
There are two ways to define the enum in the model. The simpler way is to pass the values as an array:
class User < ApplicationRecord
enum status: [:invited, :active]
end
And the second option is to pass hash with mappings:
class User < ApplicationRecord
enum status: { invited: 0, active: 1 }
end
Both definitions are working the same way. However, it is recommended to always use hash because:
- You know precisely what integers are mapped to what status
- You can change the order of the keys, and it won’t break the data integrity, while with the array option, you will damage the data by changing the order
It’s a straightforward change from an array to hash but can save you a lot of time and prevent problems if someone accidentally changes the order of enum keys.
Enum with value object design pattern
If you plan to share logic between two or more models in case of the same enums or just add more logic to enums, it is a perfect idea to build the code with the value object design pattern.
A value object is a simple Ruby object that only returns values (it does not update any data). With the proper naming, such classes are easily readable and extendable pieces of code that every developer loves.
Let’s assume that you are building application tracking system and you have the candidate model with the following statuses defined:
class Candidate < ApplicationRecord
enum status: {
submitted: 0,
viewed: 1,
reviewed: 2,
rejected: 3,
invited: 4,
interviewed: 5,
verified: 6,
offered: 7,
hired: 8,
in_trial: 9,
after_trial: 10
}
end
A lot of statuses. We would like to group them into the following groups:
- in_process - submitted, viewed, reviewed, invited, interviewed, verified, and offered
- employed - hired,
in_trial
,after_trail
Let’s create a class called CandidateStatus
that would handle the mentioned statuses:
class CandidateStatus
STATUSES = { submitted: 0, viewed: 1, reviewed: 2, rejected: 3, invited: 4, interviewed: 5,
verified: 6, offered: 7, hired: 8, in_trial: 9, after_trial: 10
}.freeze
EMPLOYED_STATUSES = %w(hired in_trial after_trial).freeze
IN_PROCESS_STATUSES = %w(viewed reviewed rejected invited interviewed verified offered).freeze
def initialize(status)
@status = status
end
def to_s
@status
end
def employed?
EMPLOYED_STATUSES.include?(@status)
end
def in_process?
IN_PROCESS_STATUSES.include?(@status)
end
end
It’s effortless. We can now update the model to return the instance of CandidateStatus
class each time the status method is called:
class Candidate < ApplicationRecord
enum status: CandidateStatus::STATUSES
def status
@status ||= CandidateStatus.new(read_attribute(:status))
end
end
With the above design we can now use meaningful methods to manage the candidate’s status:
candidate = Candidate.create(status: :reviewed)
candidate.reviewed? # => true
candidate.status.to_s # => "reviewed"
candidate.status.in_process? # => true
Our model won’t become fat, and the logic for statuses is separated in a very simple class that is easy to extend and test outside the model.
The right naming
Not always the naming convention that is available by default makes sense. Let’s consider the case where we have a model GraphicCard
with the performance column:
class GraphicCard < ApplicationRecord
enum performance: { low: 0, normal: 1, high: 2 }
end
Now we want to check if card has a high and low performance:
card = GraphicCard.create(performance: :low)
card.high? # => false
card.low? # => true
It does not make sense; what is card.low?
? To make the code more readable, we can define suffix and prefix. In our case suffix will do the work:
class GraphicCard < ApplicationRecord
enum performance: { low: 0, normal: 1, high: 2 }, _suffix: true
end
With this configuration, every enum value is ended with “_performance” string:
card = GraphicCard.create(performance: :low)
card.high_performance? # => false
card.low_performance? # => true
Now, it makes sense! You know what this code is about even without seeing the enum definition. If you passed true
either to _suffix
or _prefix
option, Rails would use the enum name (in our case performance), but you can also pass a string.
The next steps
The enum definition is simple, but it does not mean that it can’t lead to problems in the future. The right design will always make your code more extendable and readable.
If you are interested in more topics regarding enums, make sure you read the article about how enum are working under the hood