Ruby
Article

Enumerated Types with ActiveRecord and PostgreSQL

By Hendra Uzia

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

  • kc00l

    Nice insight on using enum type with Postgresql and ActiveRecord::Enum. Hopefully ActiveRecord::Enum development will allow detecting the use of an enum column type and thus avoid using awkward hashes having keys and values with the same name.

  • Nimrod

    You can also extend the enum type when needed:
    ALTER TYPE gender ADD VALUE ‘other’ AFTER ‘prefer_not_to_disclose’;

    But it seems like you cannot remove elements from the list of allowed values.

  • Andrew France

    An excellent guide. ActiveRecord’s default of storing integers is an anti-pattern from when we needed to save a precious few bytes in a row.

  • Mayra Cabrera

    Very nicely explained, great article :)

  • http://engineering.alphasights.com Tor Erik Linnerud

    Nice, didn’t know that Rails enums could work with “strings” as keys.

    In your migration you add an index to the gender column. It may be counterproductive to index an enum column with less than ~10 choices. Say you have 4 choices evenly distributed. You’ll end up having to scan 25% of the table anyway, but doing so through the index is slower than a straight table scan. The DB knows this so it will tend not to use the index. If you know that one option is rare and want to be able to look it up quickly, you’re better off with a partial index on just that value.

  • rizal muthi

    Wow, my countryman wrote a very well and informative article.
    This is very useful, Thanks :D

  • Imran Shakir

    This got me a lot closer to a solution. However, I might perhaps have missed something, I have an enum status with three values ‘pending’, ‘accepted’, ‘rejected’ and I have added this enum to the table and the model. However when I try to call TransferRequest.pending I get an error: no method to_sym for nil:NilClass. Am I missing something obvious here?

    • Dimid Duchovny

      Did you mean TransferRequest.pending? ?

  • Brad

    If you are indexing, note that add column: :users, :gender, :gender, index: true didn’t actually add an index on :users for me on Rails 4.2.3. You can confirm for yourself using your db client or via rails db and d users .

    Adding a second line add_index :users, :gender to the migration did the trick.

  • Bjarki Gunnarsson

    Brilliant tutorial! Thank you :-)

  • http://fauxpar.se/ Matt Powell

    Just a small point: when using ActiveRecord::Enum, the default value is the first one in the list. So instead of having “male” as the default, why not have “not_sure” first?

  • Fredi

    Thank you for this tutorial. Very comprehensive.

    Has anybody experienced any issues when using rails_admin gem to update a record that uses PostgreSQL enumerated types?

  • http://www.LiftHero.com/ J Connolly

    Would you recommend this approach over a standard integer column with a check constraint? I’m not sure that decoupling meaning of the types from the model is worth the extra overhead, especially since you can’t delete types from postgres enum columns.

    As a second question, when would it be productive to index a constrained integer column like that?

  • Dimid Duchovny

    Thanks, can you explain how to use it?

  • xxx44yyy

    rails 4.1.6
    When I save object then I have problems with cast model attribute to postgres enum column. But found solution. Add this string:
    ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::OID.alias_type ‘gender’, ‘text’
    to initialize

  • Zdravko Balorda

    Not sure? How stupid is that, sorry.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.