Blog

Thoughts from my daily grind

Rails ActiveRecord with PostgreSQL native enums

Posted by Ziyan Junaideen |Published: 04 June 2022 |Category: Ruby on Rails
Default Upload |

When I started Ruby in 2012, enums quickly became my favourite PostgreSQL data type. Today that mantle is carried by JSONB. I found enums immensely useful with flow control in my applications. At first, we required third-party gems for enum support, and since Rails 4.1 (in 2014), we got native AR support for enums.

The Rails 7 release brings another interesting improvement to ActiveRecord. AR now supports the creation of enum data types using the create_enum method. The most significant advantage is that it properly updates the schema.rb file.

Example

The most apparent use of enums is in a user model where I must classify a user's role.

# Defines a native enum on PostgreSQL.
class CreateUserRolesEnum < ActiveRecord::Migration[7.0]
  def up
    create_enum :user_roles, %w[admin user]
  end

  def down
    # While there is a `create_enum` method, there is no way to drop it. You can
    # how ever, use raw SQL to drop the enum type.
    execute <<-SQL
      DROP TYPE user_roles;
    SQL
  end
end
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :first_name
      t.string :last_name
      t.string :email
      t.enum :role, enum_type: :user_roles, default: :user

      t.timestamps
    end
  end
end

Then you can introduce the enum field to your application as follows.

class User < ApplicationRecord
  enum :role, { user: 'user', admin: 'admin' }
  # ...
end

Now you can do:

user = User.new(first_name: 'Jane', last_name: 'Doe', role: 'admin')
user.admin? # => true

Before we wrap up the example, let's look at the db/schema.rb file.

ActiveRecord::Schema[7.0].define(version: 2022_06_02_044813) do
  # These are extensions that must be enabled in order to support this database
  enable_extension "plpgsql"

  # Custom types defined in this database.
  # Note that some types may not work with other database engines. Be careful if changing the database.
  create_enum "user_roles", ["admin", "user"]

  # ...
end

Using the create_enum method now adds the enum types to the schema.rb file thus being useful when we set up the database using rails db:schema:load.

Native PG Enums - Rails 6.1 and below

Before Ruby on Rails 7, we would have to generate enum types using raw SQL.

# Defines a native enum on PostgreSQL.
class CreateTransactionStatusEnum < ActiveRecord::Migration[6.1]
  def up
    execute <<-SQL
      CREATE TYPE user_roles AS ENUM ('admin', 'admin');
    SQL
  end

  def down
    execute <<-SQL
      DROP TYPE user_roles;
    SQL    
  end
end

Once you define the enum data type, you can use write migrations as follows:

# If you are creating a table
create_table :users do |t|
  # ...
  t.column 'role', 'user_role'
  # ...
end

# If you are just adding a enum column
add_column :users, :role, :user_roles

Adding and removing values from an enum type

I often find myself needing to add and remove values from an enum. With the enum data, things get complicated.

class AddStaffToUserTypesEnum < ActiveRecord::Migration[7.0]
  disable_ddl_transaction!

  def up
    execute <<-SQL
      ALTER TYPE user_types ADD VALUE 'staff';
    SQL
  end

  def down
    execute <<-SQL
      ALTER TYPE user_roles RENAME TO user_roles_old;
      CREATE TYPE user_types AS ENUM ('admin', 'user');

      ALTER TABLE users
      ALTER COLUMN role
      TYPE user_roles
      USING status::text::user_roles;

      DROP TYPE user_roles_old;
    SQL
  end
end

Advantages & disadvantages

Why would you use the enum data type? To be honest, I don't. As with any feature, there are advantages and disadvantages for using PG enum data type.

Advantages:

  • Better performance and less storage (probably more useful in data warehousing)
  • Easier and meaningful SQL (not an issue since we use AR or other ORMs, all the time)
  • Don't require application-level constant checks (with AR you won't have to)

Disadvantages:

  • Not supported by all database engines (crate_enum is limited to PostgreSQL)
  • Less flexibility (in a SaaS setting, a tenant may need to define its own roles)
  • Complications with adding values (require DDL changes)
  • Complications with localization

Conclusion

I like the fact that ActiveRecord is improving around Enums. Enums have their use cases but I have always avoided using them in Rails applications. I believe the complications outweigh the benefits. I use integers (tiny ints to be particular)

add_column :users, :role, :integer, limit: 1, default: 0

ORMs like ActiveRecord and DataMapper give application developers little reason to use native PostgreSQL enums. But the updates to support enums natively is a welcoming move. create_enum is a great addition but I believe we need more support for modifying enums. I will consider using enums instead of integers once there is no need to write complicated migrations to add value to an existing type.

About the Author

Ziyan Junaideen -

Ziyan is an expert Ruby on Rails web developer with 8 years of experience specializing in SaaS applications. He spends his free time he writes blogs, drawing on his iPad, shoots photos.

Comments