Subscribe to access all episodes. View plans →
Published January 4, 2021
Elixir 1.10
Phoenix 1.5
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 uuid
s, 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.