DEV Community

Dennis Beatty
Dennis Beatty

Posted on • Originally published at dennisbeatty.com

Use the new Enum type in Ecto 3.5

Booleans are one of the oldest constructs in programming. They're simple. Binary. And we use booleans everywhere. One of my earliest software projects had a database table called "users" with several columns used to specify role like is_admin, is_moderator, and is_seller.

%User{is_admin: false, is_moderator: false, is_seller: false}
Enter fullscreen mode Exit fullscreen mode

This gets even worse when booleans are used to represent some sort of state. For example a payment service that stores its state in booleans could potentially have columns like is_authorized, is_captured, is_failed, and is_refunded. That's just complicated. And that's just with a single piece of state. Many places support partial refunds, which means now you've got another boolean is_partial_refund.

%Payment{
  is_authorized: false,
  is_captured: false,
  is_failed: false,
  is_refunded: false,
  is_partial_refund: false
}
Enter fullscreen mode Exit fullscreen mode

But there's another issue: now you can have impossible states. For instance if somehow a record has is_partial_refund set to true but is_refunded set to
false what does that even mean? Or if a record has is_failed set to true but also is_captured set to true. Make impossible states impossible!

Enums

Enums allow us to do just that. They enable a variable to contain a value from a set of predefined constants. In a statically-typed language, enum values are checked at compile time to eradicate bad values. Elixir is dynamically-typed, so we don't get that safety guarantee, but that doesn't demean enums' usefulness.

The user example above is made simpler with the use of an enum:

%User{role: :admin}
Enter fullscreen mode Exit fullscreen mode

The role field would contain one of the following atoms: :admin, :moderator, :seller, :buyer. That simplifies our user struct immensely, but the difference is even bigger with the payment example:

%Payment{status: :pending}
Enter fullscreen mode Exit fullscreen mode

Instead of setting different combinations of five boolean flags, we can change the status to one of :authorized, :captured, :failed, :refunded, or :partially_refunded. And there is no way for the payment to get into one of the impossible states mentioned above. But how does this work with a database?

Rolling your own enums with Strings

The simplest way to store "enums" in a database with Elixir and Ecto has been to roll your own enums. By creating a new varchar column on your table, you can then write the appropriate string to it when creating or updating a record.
Ecto.Changeset.validate_inclusion/4 will allow you to check that the string being provided contains one of the supported values so that you're not inputting bad data.

But that safety only exists at the application layer. It ignores the potential that someone accessing the database directly could input a bad value by bypassing the application. If a bad string gets in and the application doesn't handle it, that bad string will crash the process. But you shouldn't have to waste time validating values coming from your data layer. Your application deserves better.

EctoEnum

Postgres and MySQL have the ability to create enum types that can be used for columns and will validate them at the data layer rather than the application layer. This means that your data store would be protected from bad values. For
a long time, the EctoEnum library has been the best way to set up custom enum types for Postgres:

# lib/my_app/accounts/user_role.ex
defmodule MyApp.Accounts.UserRole do
  use EctoEnum, type: :user_role, enums: [:admin, :moderator, :seller, :buyer]
end

# lib/my_app/accounts/user.ex
defmodule MyApp.Accounts.User do
  use Ecto.Schema
  alias MyApp.Accounts.UserRole

  schema "users" do
    field :role, UserRole
  end
end

# priv/repo/migrations/20210102193646_add_role_to_users.exs
defmodule MyApp.Repo.Migrations.AddRoleToUsers do
  use Ecto.Migration
  alias MyApp.Accounts.UserRole

  def change do
    UserRole.create_type()

    alter table(:users) do
      add :role, :user_role
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

It requires creating a new module for the enum type, but it has some nice conveniences for using the type in your schema and for creating the type in your migrations in the first place. But using one more dependency also means one more thing in the application that has to be maintained.

Ecto 3.5

Ecto 3.5 was released in October 2020, and it included the new Ecto.Enum module. The module's documentation expects the Enum type to be a string when stored in the database, but we can also make it work with a Postgres enum. This means you can now use enums without needing another library. Ecto SQL still doesn't have convenience functions for creating enums in migrations (yet) so you'll have to drop down to raw SQL and do that manually:

defmodule MyApp.Repo.Migrations.AddRoleToUsers do
  use Ecto.Migration

  def up do
    execute("CREATE TYPE user_role AS ENUM ('admin', 'moderator', 'seller', 'buyer')")

    alter table(:users) do
      add :role, :user_role
    end
  end

  def down do
    alter table(:users) do
      remove :role, :user_role
    end

    execute("DROP TYPE user_role")
  end
end
Enter fullscreen mode Exit fullscreen mode

Then you can add an enum to your schema without having to create a new module and Ecto type though:

defmodule MyApp.Accounts.User do
  use Ecto.Schema

  schema "users" do
    field :role, Ecto.Enum, values: [:admin, :moderator, :seller, :buyer]
  end
end
Enter fullscreen mode Exit fullscreen mode

Just like the EctoEnum library, Ecto handles all of the casting for changesets, and the role is available as an atom rather than a string. This provides some safety because you will be able to more easily differentiate between the
unsafe string values provided by users and the safer atoms that have been validated by the application.

What do you think?

Do you like the new Ecto.Enum API, or do you wish the API was different? Are you going to stick to the EctoEnum library or to just using Strings? Let me know on Twitter!

And if you liked this post, hopefully you'll like the others I write. Subscribe to my email newsletter and get a notification whenever I write a new one!

Happy New Year!

Top comments (2)

Collapse
 
nathanjohnson320 profile image
Nathaniel Johnson

This is really helpful!

Do you know if there's an open issue in ecto for creating the type migrations that I can follow?

Collapse
 
dnsbty profile image
Dennis Beatty

There isn't that I know of. And to be completely honest, I'm not sure if there ever will be just because MySQL and SQL Server and other Ecto backends don't really allow you to create enums like Postgres does, and the Ecto team likes to keep things generic.