Enumerated Types with ActiveRecord and PostgreSQL

Share this article

An Enumerated Type (also called an “enum”) is a data type with a set of named elements, each of them having a different value that can be used as constants, among other things. An enum can be implemented in various languages and frameworks, and in this article we will implement an enum in a Ruby on Rails application.

Rails, as you likely know, is an open source MVC web application framework. The M in MVC is ActiveRecord, which is an Object Relational Mapping Framework. ActiveRecord gives us the ability to represent models, their data, and other database operations in an object-oriented fashion. An enum is treated specially in ActiveRecord, being implemented using the ActiveRecord::Enum module.

Rails provides a very good usage of ActiveRecord::Enum, and to have a better architecture, we need to use it with the PostgreSQL Enumerated Type. Unfortunately, there is little or no mention on how to integrate these technologies. There’s even a statement that says it has no special support for enumerated types, although it is possible.

In this article, we’ll go over how to use PostgreSQL Enumerated Types with ActiveRecord::Enum.

This article is divided into 3 sections as follows:

  • Introduction to ActiveRecord::Enum
  • Introduction to PostgreSQL Enumerated Types
  • Integrating ActiveRecord::Enum with PostgreSQL Enumerated Types

If you are already familiar with any of the above, feel free to skip it and jump into the one you like.

Introduction to ActiveRecord::Enum

ActiveRecord::Enum was introduced in Rails 4.1, as announced in the release notes. It provides the ability to manage enums, mutate their values, and scope a model against any available enum value. Before you can use ActiveRecord::Enum, there are 3 aspects that you need to know: database migration, declaration, and usage.

This article will use the following use case to implement ActiveRecord::Enum. We will add gender attribute to a User model, with the following possible values:

  • Male
  • Female
  • Not sure
  • Prefer not to disclose

Database Migration

ActiveRecord::Enum uses the integer data type to store the value of the enum in the database. Let’s create the database migration as follows:

bundle exec rails generate migration AddGenderToUsers gender:integer

The above code will generate a new migration:

# db/migrate/20150619131527_add_gender_to_users.rb
class AddGenderToUsers < ActiveRecord::Migration
  def change
    add_column :users, :gender, :integer
  end
end

We can add a default value to the migration. And to increase application performance, we can also add an index to the column by adding the following code:

# db/migrate/20150619131527_add_gender_to_users.rb
class AddGenderToUsers < ActiveRecord::Migration
  def change
    # add `default: 3`, and `index: true`
    add_column :users, :gender, :integer, default: 0, index: true
  end
end

Finally, run the migration:

bundle exec rake db:migrate

We are done with the database migration. Next, we need to declare the ActiveRecord::Enum to the model.

Declaration

There are 2 kinds of declaration for ActiveRecord::Enum. Using our use case, we will add gender to the User model. The first declaration is using Array:

# app/models/user.rb
class User < ActiveRecord::Base
  enum gender: [ :male, :female, :not_sure, :prefer_not_to_disclose ]
end

The above code will store male value to 0, and the rest of the values mapped with their order in the array. Future additions must be added to the end of the array, and reordering or removing values must not be done. In order to be able to manipulate values later, use the second type of declaration using Hash:

# app/models/user.rb
class User < ActiveRecord::Base
  enum gender: { male: 0, female: 1, not_sure: 2, prefer_not_to_disclose: 3 }
end

With this declaration, we can reorder and remove any value. The value can start from any number, as long as it is aligned with the default value in the database migration. Future additions can be put in any position.

Usage

There are 3 aspects of ActiveRecord::Enum usage, which are: mutation, retrieval, and scoping. Helper methods were provided to do those 3 things. With our use case, we can mutate the user’s gender as follows:

# mutate enum using the exclamation mark (!) method.
user.male!
user.female!
user.not_sure!
user.prefer_not_to_disclose!

# or mutate enum using value assignment
user.gender = nil
user.gender = "male"
user.gender = "female"
user.gender = "not_sure"
user.gender = "prefer_not_to_disclose"

To retrieve the enum value:

# retrieve enum value using the question mark (?) method
user.male?                   # => false
user.female?                 # => false
user.not_sure?               # => false
user.prefer_not_to_disclose? # => true

# or retrieve using the enum name
user.gender                  # => "prefer_not_to_disclose"

To scope the User model to the enum values, use the following methods:

# scope using enum values
User.male
User.female
User.not_sure
User.prefer_not_to_disclose

# or we can scope it manually using query with provided class methods
User.where("gender <> ?", User.genders[:prefer_not_to_disclose])

As previously mentioned, ActiveRecord::Enum uses integers to stores the value in the database. Unfortunately, the enum value in the database becomes less meaningful and is not self-explanatory. Enum values becomes dependent on a model, becoming highly coupled with that model. To uncouple the enum value from a model, we need to store it using an enumerated type in the database.

Introduction to PostgreSQL Enumerated Types

PostgreSQL provides Enumerated Type to store a static and ordered set of values. Let’s take a look at how this is implemented in PostgreSQL. With our use case, we can create a gender type as follows:

CREATE TYPE gender AS ENUM ('male', 'female', 'not_sure', 'prefer_not_to_disclose');

Once created, we can use the type in our table like any other type. As an example, we can create users table with a gender type attribute as follows.

CREATE TABLE users (
    name text,
    gender gender
);

By using an enum instead of an integer, we gain type safety. That means you cannot add a value that is not part of the enum values. Here is an example of the type safety feature:

INSERT INTO users(name, gender) VALUES ('John Doe', 'male');
INSERT INTO users(name, gender) VALUES ('Confused John Doe', 'not_sure');
INSERT INTO users(name, gender) VALUES ('Unknown John Doe', 'unknown');
ERROR: invalid input value for enum gender: "unknown"

An enumerated type is case sensitive, that means male is different from MALE. White space is also significant.

Integrating ActiveRecord::Enum with PostgreSQL Enumerated Types

There are 2 things that we need to do before we can use ActiveRecord::Enum with PostgreSQL Enumerated Types: database migration and enum declaration.

First, let’s create the database migration:

bundle exec rails generate migration AddGenderToUsers gender:gender

Next, edit the generated migration to add the type:

# db/migrate/20150619131527_add_gender_to_users.rb
class AddGenderToUsers < ActiveRecord::Migration
  def up
    execute <<-SQL
      CREATE TYPE gender AS ENUM ('male', 'female', 'not_sure', 'prefer_not_to_disclose');
    SQL

    add_column :users, :gender, :gender, index: true
  end

  def down
    remove_column :users, :gender

    execute <<-SQL
      DROP TYPE gender;
    SQL
  end
end

Once you’re finished with that, run the migration:

bundle exec rake db:migrate

Now, we have completed the database migration. The next step is to declare an enum in the User model. Earlier, we used both the Array and Hash forms to declare an enum. For the integration to work, we need to declare an enum using the Hash form:

# app/models/user.rb
class User < ActiveRecord::Base
  enum gender: {
    male:                   'male',
    female:                 'female',
    not_sure:               'not_sure',
    prefer_not_to_disclose: 'prefer_not_to_disclose'
  }
end

Finally, we can store ActiveRecord::Enum values using PostgreSQL Enumerated Types. As a bonus, all helper methods provided by ActiveRecord::Enum still work as expected.

Conclusion

ActiveRecord::Enum, by default, stores values using integers. The value is meaningless without the model, and it is not self-explanatory. In other words, the enum value is highly coupled with a model. To have a better architecture, we need to uncouple the enum value from a model.

PostgreSQL Enumerated Types provide a good complement for ActiveRecord::Enum. We can have a meaningful, and self-explanatory data that doesn’t require a model to decipher the value. With the type safety feature provided by PostgreSQL, we can have a solid foundation for storing ActiveRecord::Enum values.

I hope you found this useful. Thanks for reading

Frequently Asked Questions (FAQs) about Enumerated Types with ActiveRecord and PostgreSQL

What are Enumerated Types in PostgreSQL and why are they important?

Enumerated Types, or Enums, in PostgreSQL are user-defined data types that allow you to create a list of predefined values. They are important because they help to ensure data integrity by limiting the possible values that can be entered into a column. This can be particularly useful when you want to restrict a column to a specific set of values, such as days of the week or status types.

How do I create an Enumerated Type in PostgreSQL?

To create an Enumerated Type in PostgreSQL, you can use the CREATE TYPE command followed by the name of the type and the values it can take. For example, to create a type for days of the week, you could use the following command:
CREATE TYPE day_of_week AS ENUM ('Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday');
This will create a new type called day_of_week that can take any of the seven days of the week as its value.

How do I use Enumerated Types with ActiveRecord?

ActiveRecord, the ORM used by Ruby on Rails, provides a built-in method for working with Enumerated Types. You can define an enum in your model using the enum method, passing it a hash where the keys are the names of the enum values and the values are the corresponding integers. For example:
class User < ApplicationRecord
enum status: { active: 0, inactive: 1 }
end
This will create a status enum on the User model, with two possible values: active and inactive.

What are the benefits of using Enumerated Types with ActiveRecord?

Using Enumerated Types with ActiveRecord provides several benefits. Firstly, it allows you to enforce data integrity at the database level, ensuring that only valid values can be entered into a column. Secondly, ActiveRecord provides a number of helper methods for working with enums, making it easy to query and update enum values. Finally, using enums can make your code more readable and self-explanatory, as the possible values for a column are clearly defined in your model.

Can I add or remove values from an Enumerated Type in PostgreSQL?

Yes, you can add or remove values from an Enumerated Type in PostgreSQL, but it’s not as straightforward as you might think. To add a value, you can use the ALTER TYPE command with the ADD VALUE clause. However, this can only be done if the type is not being used by any column in any table. To remove a value, you would need to create a new type without the unwanted value, change the type of the column to the new type, and then delete the old type.

How do I query Enumerated Types in ActiveRecord?

ActiveRecord provides several helper methods for querying Enumerated Types. For example, if you have a status enum on your User model, you can use the following methods to query users based on their status:
User.active
User.inactive
These methods will return all users with the corresponding status.

Can I use Enumerated Types with other databases?

Yes, Enumerated Types are a standard feature of SQL and are supported by many relational databases, including MySQL, Oracle, and SQL Server. However, the syntax for creating and manipulating Enumerated Types may vary between different databases.

What are the drawbacks of using Enumerated Types?

While Enumerated Types have many benefits, they also have a few drawbacks. One of the main drawbacks is that adding or removing values can be a complex process, particularly if the type is being used by a column in a table. Additionally, because Enumerated Types are user-defined, they may not be supported by all database tools or ORMs.

Can I use Enumerated Types in migrations?

Yes, you can use Enumerated Types in migrations. In fact, this is often the best place to define your Enumerated Types, as it allows you to keep track of changes to your database schema over time. However, keep in mind that adding or removing values from an Enumerated Type in a migration can be a complex process.

How do I handle Enumerated Types in a multi-database Rails application?

Handling Enumerated Types in a multi-database Rails application can be a bit tricky, as different databases may have different syntax for creating and manipulating Enumerated Types. One approach is to use the lowest common denominator of features supported by all your databases. Alternatively, you could use database-specific migrations to handle Enumerated Types differently for each database.

Hendra UziaHendra Uzia
View Author

Hendra is the software engineer of vidio.com, the largest video sharing website in Indonesia. He also contributes to the open source community on rspec-authorization and minitest-around.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week