Rails ActiveRecord with PostgreSQL native enums
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.