Subscribe to access all episodes. View plans →

#127: UUID Primary Key with Ecto

Published January 4, 2021

Elixir 1.10

Phoenix 1.5

Ecto Schema attributes

Phoenix Param


In this episode let’s see how we can use a universally unique identifier or UUID as the primary key for a schema. To start we’ll need a schema to work with so let’s create a “user”.

We’ll go to the command line and use the Phoenix HTML generator with the context module of Accounts followed by the schema module User the table name users and let’s give it a single field of name, which will be a string.

$ mix phx.gen.html Accounts User users name
  * creating lib/teacher_web/controllers/user_controller.ex
  * creating lib/teacher_web/templates/user/edit.html.eex
  * creating lib/teacher_web/templates/user/form.html.eex
  * creating lib/teacher_web/templates/user/index.html.eex
  * creating lib/teacher_web/templates/user/new.html.eex
  * creating lib/teacher_web/templates/user/show.html.eex
  * creating lib/teacher_web/views/user_view.ex
  * creating test/teacher_web/controllers/user_controller_test.exs
  * creating lib/teacher/accounts/user.ex
  * creating priv/repo/migrations/{timestamp}_create_users.exs
  * creating lib/teacher/accounts.ex
  * injecting lib/teacher/accounts.ex
  * creating test/teacher/accounts_test.exs
  * injecting test/teacher/accounts_test.exs

This generates the controller, views, and templates for our HTML resource. Let’s open the generated {timestamp}_create_users.exs database migration. There we see the migration we would expect to create the users table.

Because we don’t want to use the typical primary key and instead a UUID, let’s include the primary_key: false option. Then we’ll want to add the field for our uuid. We’ll add a new field named uuid with the database type of uuid and let’s include the primary_key: true option since we want to use this as the primary key for our users table.

priv/repo/migrations/{timestamp}_create_users.exs

defmodule Teacher.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users, primary_key: false) do
      add :uuid, :uuid, primary_key: true
      add :name, :string

      timestamps()
    end
  end
end

Now before we run our migration let’s open the generated corresponding user.ex module. Since we want to use the uuid for our schema’s primary key, we need to configure that here. Ecto provides schema attributes that help us do just that.

We can use the @primary_key attribute to tell Ecto to use the uuid field as the primary key. It expects a tuple with the field name uuid, the type binary_id, and any options. We want the uuid to be autogenerated too let’s include autogenerate: true.

lib/teacher/accounts/user.ex

defmodule Teacher.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:uuid, :binary_id, autogenerate: true}

  schema "users" do
    field :name, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end

Then let’s open our router.ex and add our “users” resource.

lib/teacher_web/router.ex

...
scope "/", TeacherWeb do
  pipe_through :browser

  resources "/users", UserController

  live "/", PageLive, :index
end
...

Then we can go back to the command line and run our migration.

$ mix ecto.migrate
...

Now let’s try to create a user to test that our uuid is generated. We’ll go to the command line…and start an IEx session with our project. Once it starts up, we can call Accounts.create_user to create a user.

Great - it looks like a user was created and we can see the uuid was set. Now every time we create a user their uuid will be automatically generated for us.

$ iex -S mix
> Teacher.Accounts.create_user(%{"name" => "Bob Dylan"})
{:ok,
 %Teacher.Accounts.User{
   __meta__: #Ecto.Schema.Metadata<:loaded, "users">,
   inserted_at: ~N[2020-12-28 19:00:53],
   name: "Bob Dylan",
   updated_at: ~N[2020-12-28 19:00:53],
   uuid: "f3f9211f-0819-4147-973d-dacc06524553"
 }}

This approach works well if we only wanted to use the uuid as the primary key for our user schema, but if we wanted to use this as an application wide default, the Ecto docs give us a simpler alternative. We can create a schema module to use as a base for our application. Let’s see how this would work in our application.

We’ll create a new module named schema.ex in “lib/teacher” and define our module. Then we’ll need to define the __using__ macro. Then inside it we’ll call the quote macro and inside of that we’ll call use Ecto.Schema and the @primary_key schema attribute that we had in our user.ex module. Because our primary keys are uuids, let’s inlcude @foreign_key_type :binary_id in case we need to use any belongs_to associations in the future.

If you’re not familiar with how macros work don’t worry - all you need to know here is that everything inside of the quote will be included in any module where we use this Teacher.Schema module. This will help us from having to repeat ourselves in every schema where we want to use the uuid as the primary key.

lib/teacher/schema.ex

defmodule Teacher.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      @primary_key {:uuid, :binary_id, autogenerate: true}
      @foreign_key_type :binary_id
    end
  end
end

Then we can go back to our user.ex module and update it to use the new Teacher.Schema. With that added we can remove the @primary_key attribute since it will be included from Teacher.Schema.

lib/teacher/accounts/user.ex

defmodule Teacher.Accounts.User do
  use Teacher.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string

    timestamps()
  end

  ...
end

Great, now whenever we create a new module that uses the uuid as the primary key we can just use our new Schema module.

Now let’s test one last thing. Let’s go to the command line and start our server.

$ mix phx.server
...

And if we try to open the “/users” route in the browser, we get an error: “structs expect an :id key when converting to_param or a custom implementation of the Phoenix.Param protocol”.

This is because we’re using the uuid as the field for our primary key and Phoenix doesn’t know how to convert that to a param for us. We’ll need to configure it to use the uuid field. Let’s go back to our schema.ex module. And because the Phoenix.Param protocol is derivable we can call @derive specifying Phoenix.Param and then the key we want to use. In this case the uuid.

lib/teacher/schema.ex

defmodule Teacher.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      @primary_key {:uuid, :binary_id, autogenerate: true}
      @foreign_key_type :binary_id
      @derive {Phoenix.Param, key: :uuid}
    end
  end
end

With that let’s go back to the browser and if we refresh the page - it works! We can now view all our users in the UI and their UUIDs are being used.

© 2024 HEXMONSTER LLC