Simplifying Using Enum In Rails

Madhuri Shrestha May 12, 2024

Ruby was built with a vision of providing a high level, genuine object-oriented programming language that offers ease of use and understanding. With time, the language has evolved and together with the framework Rails, it has persistently been shaping productive solutions with simplicity.The use of ENUM in Ruby On Rails is one of the ways through which we can explore this aspect.

If you are unfamiliar with ENUMs, enum is a data type used to assign names to integer values that behave as a constant. With the introduction of names to the 0s and 1s of the data, it provides readability and aids in its maintenance through the self-explanatory codebase.

Let’s explore how we can leverage enums.

Setting Up The Application For Using enum

The first step would be to create a table with an enum column or add a column to the existing table in the database. This is pretty straightforward as adding an integer field would do the trick.

Let us set up a table for tracking candidate progression for a HR company.


rails g migration CreateCandidates name:string status:integer

This migration creates a table Candidate with a string field name and an enum field status.

ruby


class CreateCandidates < ActiveRecord::Migration[6.1]
  def change
    create_table :candidates do |t|
      t.string :name
      t.integer :status

      t.timestamps
    end
  end
end

For an existing table Candidate, if we want to add a new enum field called status, we add the following migration.


rails g migration AddStatusToCandidates status:integer

The generated migration file looks like below:

ruby


class AddStatusToCandidates < ActiveRecord::Migration[6.1]
  def change
    add_column :candidates, :status, :integer
  end
end

Then, we simply run the migration to reflect these changes:


bundle exec rake db:migrate

Now that we have set up an enum field, let’s explore how we can use it.

Declaring enum

A candidate goes through a progression of being at the state of applied, shortlisted, interviewed, hired or rejected.

We can represent this progression in the Candidate model as an array or a hash where we start with enum, then the column name and then the collection of values. By default the declared enums can be used as a scope. If we do not want them to be used as scope, we can disable it by explicitly stating scopes: false.

Enum values as an array

When representing the collection of values as an array, we represent it as a collection of symbols in either of the ways shown below.

ruby

# frozen_string_literal: true

class Candidate  < ApplicationRecord
  enum status: [:applied, :shortlisted, :interviewed, :hired, :rejected], 
  scoped: false
end

OR

ruby


# frozen_string_literal: true

class Candidate  < ApplicationRecord
  enum status: %i(applied shortlisted interviewed hired rejected)
end

The integer values represented by the collection of status are 0, 1, 2, 3 and 4 in their respective order.

Enum values as a hash

When representing the collection of values as a hash, we have the flexibility of assigning any integer value to the collection.

ruby


# frozen_string_literal: true

class Candidate < ApplicationRecord
  enum status: { applied: 0, shortlisted: 1, interviewed: 2, hired: 3, rejected: 4 }
end

OR

ruby


# frozen_string_literal: true

class Candidate < ApplicationRecord
  enum status: { applied: 1, shortlisted: 3, interviewed: 5, hired: 7, rejected: 9 }
end

Using either an array or a hash for declaring enums will yield us the same output when we fetch for the values.

ruby


(main)> Candidate.statuses
=> {"applied"=>0, "shortlisted"=>1, "interviewed"=>2, "hired"=>3, "rejected"=>4}

But it is recommended to use hash to declare the enums since the values of an array enum is strictly dependent on their order. So a mixup of the order can corrupt the data.

Using enum

We can use the declared enum values in all the CRUD operations.

Create

ruby


(main)> Candidate.create(name: 'First Test Candidate', status: "applied")
=> #<Candidate:0x000000011612eaf0 id: 1, name: "First Test Candidate", 
created_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
updated_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
status: "applied">
Read

ruby


(main)> Candidate.where(status: :applied)
=> [#<Candidate:0x000000010331e968 id: 1, name: "First Test Candidate", 
created_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
updated_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
status: "applied">]

The enums can also be used as scope for query:

ruby


(main)> Candidate.applied
=> [#<Candidate:0x0000000115847108 id: 1, name: "First Test Candidate", 
created_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
updated_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
status: "applied">]

ruby


(main)> candidate = Candidate.find 1
=> #<Candidate:0x0000000110f72c98 id: 1, name: "First Test Candidate", 
created_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
updated_at: Sun, 12 May 2024 12:55:10.172380000 AEST +10:00, 
status: "applied">
(main)> candidate.applied?
=> true
Update

Updating the status via assignment:

ruby


(main)> candidate.status = :shortlisted
=> :shortlisted
(main)> candidate.save!
=> true
(main)> candidate.shortlisted?
=> true

Or using the ! method:

ruby


(main)> candidate.interviewed!
=> true
(main)> candidate.interviewed?
=> true
Delete

ruby


(main)> Candidate.interviewed.delete_all
=> 1
(main)> Candidate.count
   (34.9ms)  SELECT COUNT(*) FROM "candidates"
=> 0

Same is for

ruby


Candidate.create(name: "First Test Candidate", status: :applied)

Conclusion

Use of enum can be a great way to simplify and represent numerical progressions of situations for better legibility.