Enumerated Types in Ruby on Rails with Postgres

(originally published at https://jasonfleetwoodboldt.com/courses/stepping-up-rails/enumerated-types-in-rails-and-postgres/)

MEET ENUMERATED TYPES

An enum is an enumerated type. In Rails, this means that you define a specific list of allowed values up front.

Historically, you did this with an enum defined on your model, like so:

enum status:  [:pending, :in_progress: :finished]

You would then create an integer field on the model. Rails will map the :pending type to the integer 0, the :in_progress key to the integer 1, and :finished to the integer 2.

You can then refer in your code to the symbol instead of the integer, which removes the dependency on the underlying integer implementation from being sprinkled throughout your code.

You can write this in your code:

Conversation.where.not(status: :pending)

Instead of writing this:

Conversation.where.not(status: 0)

That’s because by writing “0” into your code, you’ve now created a dependency on that part of the code knowing that 0 means “pending.” You’ve also slowed the next developer because now they have to remember that 0 means “pending.”

Using the Ruby symbol instead (:pending), you’re making your code less brittle and de-complected.

So it is preferred when you can to code this way.

WHAT DOES POSTGRES INTRODUCE?

Described above is historical way of creating enums in Rails — using an integer as the underlying database field type. However, with Postgres, we can now use the native Postgres enum type. That means that we need to define the enumerated type list in Postgres itself. (Unfortunately this is an extra thing to think about in the database migration which you will see below.)

Importantly, the underlying Postgres field type will be an enum type, and we will also use the Rails enum mechanism in our Ruby model definition. The two will be mapped together, but to do so we need just a couple of steps of special setup.

Natively, Rails doesn’t know about the enum database field type. That’s because Rails was written to be database agnostic and other databases don’t have enum. For this reason, if we add enum field types to our database, we’ll need to use a gem.

gem 'activerecord-pg_enum'

.

This gem does two important things:

  1. When Rails is building your schema file (db/schema.rb), it won’t be able to create the database definition if it has enum types in it. Instead of outputting a proper schema file, it will not output the database with the enum types.

Why are we doing this?

The default integer field type is bigint, which takes up 8 bytes of memory. From the activerecord-pgenum docs:

As you will see, we’re going to need to tell Postgres about our enumerated types (in schema migrations), which will be in addition to telling Rails in our model definitions. Importantly, our Rails enum definitions will no longer use arrays, instead they will use a hash. The above example will become

enum status: [:pending: 'pending', in_progress: 'in_progress', finished: 'finished']

Although this seems strangely redundant, this is the most performant and preferred way to implement enums using Postgres & Rails

Let’s get started. I assume you are starting from scratch, but if you already have an app with enums in it, you may have to migrate your data to add enums to Postgres.

STEP 1: INSTALL GEM ACTIVERECORD-POSTGRES

Add to your Gemfile

gem 'activerecord-pg_enum'

Then run bundle install

STEP 2: CREATE YOUR FIRST POSTGRES ENUM

Option A: You are generating a new Model

Once the gem is installed, you can use enum as a first-class field type, like so:

rails generate model Conversation status:enum

In the migration file, add the content shown in bold below:

class CreateConversations < ActiveRecord::Migration[7.0]
def change
create_enum "statuses", %w[pending in_progress finished] create_table :conversations do |t|
t.enum :status, as: :statuses t.timestamps
end
end
end

Note that if you fail to add

, as: :statuses

Your migration will note work.

As well, in the example above, note that the field name is status but the enumerated type itself is called statuses. Be careful when using plural/singular version of the same word to refer to similar things in your app.

Option B: Add to an Existing Model

Let’s assume you already have a Conversation model. Now you would run this migration

rails generate migration AddStatusToConversation

Here, you’ll edit the generate migration like so:

class AddStatusToConversation < ActiveRecord::Migration[7.0]
def change
create_enum "statuses", %w[pending in_progress finished]
add_column :conversations, :status, :statuses
end
end

What’s important to note here is that statuses, which is used as the 3rd argument in the add_column above, is used to specify the type of field. Normally here you might see :integer or :string. With enumerated types, we’ve defined our own “type” in Postgres’s terminology, so we can now refer to this as a field type itself.

We can only do this because we created the enum using create_enum in the line above, of course.

STEP 3: DEFINE YOUR MODEL

class Conversation
include PGEnum(statuses: %w[pending in_progress finished])
end

This is the preferred way to setup your enums, although you can still use the old style hash syntax which is equivalent:

enum status: {pending: 'pending', in_process: 'in_process', finished: 'finished'}

Now, in our code we will refer to any place we have a status as a symbol(not an integer or a symbol).

To demonstrate, we can now refer to a status on a Conversation object as either :pending, :in_progress, or :finished. Rails will translate the symbols to the Postgres enums, which in turn provide the fastest and most performant database experience.

2.7.2 :001 > conv = Conversation.new
=> #<Conversation:0x000000011e9b62d8 id: nil, created_at: nil, updated_at: nil, status: nil>
2.7.2 :002 > conv.status = :pending
=> :pending
2.7.2 :003 > conv.save
TRANSACTION (0.3ms) BEGIN
Conversation Create (0.9ms) INSERT INTO "conversations" ("created_at", "updated_at", "status") VALUES ($1, $2, $3) RETURNING "id" [["created_at", "2021-10-07 15:10:23.640882"], ["updated_at", "2021-10-07 15:10:23.640882"], ["status", "pending"]]
TRANSACTION (6.5ms) COMMIT
=> true

REMOVING THE ENUM COMPLETELY

drop_enum "status_list", %w[pending in_progress finished]

How to drop the enum entirely

You can also drop the entire enum, but this requires that there are no fields that depend on it!!!

This means Postgres is protecting you from making any records orphaned by your dropping the enum value. Instead of doing it, Postgres will give you this error:

Caused by:
ActiveRecord::StatementInvalid: PG::DependentObjectsStillExist: ERROR: cannot drop type color because other objects depend on it
DETAIL: column color of table alerts depends on type color
HINT: Use DROP ... CASCADE to drop the dependent objects too.

Choice #1) Remove all fields that point to any enum you want to drop. Set all of them to another enum or remove the fields themselves.

Choice #2) Unfortunately because the activerecord-pgenum gem does not have a way to pass the CASCADE flag to the migration if you really want to do that you’ll need to write the migration yourself. This is probably a good thing because it is enforcing good hygiene on you for your data maintenance. Go with choice #1 and avoid CASCADE.

REMOVING AN ENUM TYPE

You can’t! Sorry, you can only rename.

Example App here.

--

--

--

The Answer is Automated Testing

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Tutorial Fuzzy Logic Mamdani for Arduino

Tutorial Fuzzy Logic Mamdani for Arduino

Delays in app delivery to Kubernetes

S-WALLET MOBILE APP CONTEST🤑🤩

Discover the “Art of Software Development” in 7 Tips

Software Development Life Cycle Steps

Pub/Sub: Send a million messages per second and save thousands of $ a month using Avro

Beacon Global Discord community open

Quad Trees. (Insert Witty Title Here)

REST API Testing- Part 1

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Jason Fleetwood-Boldt

Jason Fleetwood-Boldt

The Answer is Automated Testing

More from Medium

ActiveModel Series ActiveModel:API

ruby_cool_kid.rb — Blocks, Proc and Lambdas the close siblings. Part 3

Postgres GIN Index in Rails

Ruby on Rails in a Nutshell