Dismiss

Subscribe to the newsletter

February 12, 2022

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.

Leave a reply

Sign in or Sign up to leave a response.

0 responses

Est. reading time: 5 minutes
Stats: 577 views

Categories

Collection

Part of the Ruby on Rails collection