February 12, 2022
•Last updated November 13, 2023
How to use enums in Ruby on Rails
Ruby on Rail's ships with a module known as Enum
which has a parent class of ActiveRecord
. This handy module allows you to declare different states in the database using any Model in your Rails application.
Enums are powerful thanks to the built-in methods and scopes that come with the framework. So rather than rolling all your methods in a given model, you can use a convention-based approach to manipulate a given state of something in an app.
Creating an enum
Putting enums to practice is the best way to learn so I'll create a simple blog to demonstrate.
rails new enum_demo
After the app is created we can scaffold a new Post
model.
rails g scaffold Post title content:text status:integer
invoke active_record
create db/migrate/20220211171223_create_posts.rb
create app/models/post.rb
invoke test_unit
create test/models/post_test.rb
create test/fixtures/posts.yml
invoke resource_route
route resources :posts
invoke scaffold_controller
create app/controllers/posts_controller.rb
invoke erb
create app/views/posts
create app/views/posts/index.html.erb
create app/views/posts/edit.html.erb
create app/views/posts/show.html.erb
create app/views/posts/new.html.erb
create app/views/posts/_form.html.erb
create app/views/posts/_post.html.erb
invoke resource_route
invoke test_unit
create test/controllers/posts_controller_test.rb
create test/system/posts_test.rb
invoke helper
create app/helpers/posts_helper.rb
invoke test_unit
invoke jbuilder
create app/views/posts/index.json.jbuilder
create app/views/posts/show.json.jbuilder
create app/views/posts/_post.json.jbuilder
The main column to pay attention to is the status
column which has the type of integer
. Enums use integers to look up a given state. The integer maps to a value you set in your model.
Before we migrate this change let's add a default value for the status
column.
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title
t.text :content
t.integer :status, default: 0
t.timestamps
end
end
end
You can skip this step if you prefer to set a default in the model which I'll discuss coming up.
Let's migrate our changes
rails db:migrate
Declaring enum values
Using the Post
model as an example I'll define a few new enums that map to our status column in the database.
class Post < ApplicationRecord
enum :status, [:published, :draft, :archived]
end
Using the enum
declaration we pass the status
column as a symbol in Ruby. That then accepts an Array
or Hash
of values depending on your needs.
Because the column is a type of integer
using an array will assign each spot in the array as a status type.
Behind the scenes that means for the values I passed it would resemble the following:
published => 0
draft => 1
archived => 2
Using an Array comes with some quirks. Adding a new status for example needs not to be taken lightly.
If you swap ordering of the array then the statuses will get disrupted as well.
A good rule of thumb is to not change these once set and only add to the end of the Array
for any new statuses.
On the other hand, if you prefer more control, a Hash
is a better option.
class Post < ApplicationRecord
#enum :status, [:published, :draft, :archived]
enum :status, { published: 0, draft: 1, archived: 2 }
end
While ordering here still matters, you can assign the integer to the corresponding status manually. Adding new values can still come at the end of the hash but now you are free to reorder and pass the integer you prefer.
Built-in methods with enums
Based on the values you pass to the enum
in your model you get automatic helper methods for free with Rails.
In my case that means I get the following:
post.published! # => sets post status to 0
post.published? # => true
post.status # => "published"
post.draft! # => sets post status to 1
post.draft? # => true
post.status # => "draft"
post.archived! # => sets post status to 2
post.archived? # => true
post.status # => "archived"
Built-in scopes with enums
If changing status wasn't easy enough you also get handy scopes for each status for free with enums.
Post.published
Post.not_published
Post.draft
Post.not_draft
Post.archived
Post.not_archive
Behind the scenes that's ActiveRecord making a new query on-demand:
# examples of a published ActiveRecord query
Post.where(status: :published)
Post.where.not(status: :published)
Turn off scopes
Sometimes you need more customized scopes and not the defaults. There's an option for that!
class Post < ApplicationRecord
# enum :status, [:published, :draft, :archived]
# enum :status, { published: 0, draft: 1, archived: 2 }
enum :status, { published: 0, draft: 1, archived: 2 }, scopes: false
end
Adding a trailing scopes: false
option turns the scopes off.
Adding a default status in the model
I mentioned before you don't necessarily need a default at the database level though it's a good practice. In your model you can define it like so:
class Post < ApplicationRecord
# enum :status, [:published, :draft, :archived]
# enum :status, { published: 0, draft: 1, archived: 2 }
# enum :status, { published: 0, draft: 1, archived: 2}, scopes: false
enum :status, { published: 0, draft: 1, archived: 2}, default: :published
end
Prefix/Suffix
You can use either a :prefix
or :suffix
option when defining multiple enums. This might be handy for an app of great scale.
A suffix
looks like the following:
class Post < ApplicationRecord
enum :status, { published: 0, draft: 1, archived: 2}, suffix: true
end
Adding this option means your helper methods come with a new naming convention attached to the column name.
post.published_status!
post.published_status?
With a prefix
option you get the opposite result with a bit more control over naming conventions.
class Post < ApplicationRecord
enum :status, { published: 0, draft: 1, archived: 2}, prefix: :posts
end
In our case I'm using :posts
since it related directly to the model. The helper methods then look like the following:
post.posts_published!
post.posts_published?
Wrapping up
Enums looked very foreign to me when first learning of them. Only until I did more research did I realize how much of a time-saver they can be. With Rails automating a lot of the work any type of state-based column becomes quite trivial and standardized to work with.
Categories
Collection
Part of the Ruby on Rails collection
Products and courses
-
Hello Hotwire
A course on Hotwire + Ruby on Rails.
-
Hello Rails
A course for newcomers to Ruby on Rails.
-
Rails UI
UI templates and components for Rails.