How to: use UUIDs as Primary Keys in Elixir/Phoenix

Because we needed some more resources for this.

Meryl Dakin
Flatiron Labs

--

Recently our team has been transitioning to a more microservice ecosystem. To maintain our data integrity, we decided to assign UUIDs as primary keys for the resources we’re creating in our newest application, so that other apps can have a static reference point for the data they’re receiving from us.

No fluff in this post, we’re going to get right to how you can set this up yourself!

1. Define a custom schema

You can absolutely write these options on every schema you want them used in, but in our project we’re using them for most of our models. So we made this:

# /lib/course_conductor/schema.exdefmodule CourseConductor.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

Then in each schema we want to autogenerate uuids, we just use CourseConductor.Schema. If we ever don’t want it, we just use Ecto.Schema instead.

2. Migrations

Let’s say we’re making this for the Disciplines model. Start with your table migration:

# migration for disciplines tabledef change do
create table(:disciplines, primary_key: false) do
add :uuid, :uuid, primary_key: true
add :name, :string
end
end

Notice we’re going to instruct it not to make an auto-incrementing PK upon creation by passing the primary_key: false option to create_table. Then we’ll give it a uuid column and note that IT will be our PK by passing primary_key: true.

3. Schema and Changeset

You don’t have to do anything weird here, just don’t mention uuid as a parameter you can pass in since it will be auto-generated via our migrations. We have name as the configurable parameter in our Discipline struct so here’s what our schema and changeset will look like:

# schema and changeset for Disciplineschema "disciplines" do
field(:name, :string)
field(:short_name, :string)
end
def changeset(discipline, attrs \\ %{}) do
discipline
|> cast(attrs, [:name])
|> validate_required([:name])
|> unique_constraint(:name)
end

Just treat uuid like you would an id by not mentioning it at all.

These are all you have to do to make this work! Now if you want to reference related models like you normally would with an id when you’re preloading your has_many/belongs, read on.

4. Relations: has_many/belongs_to

Our Discipline has many courses, so here’s how we can model that with UUIDs as our primary key:

  • The Discipline migration does not change.
  • The Course table migration is as follows:
# migration for courses_tabledef change do
create table(:courses, primary_key: false) do
add :uuid, :uuid, primary_key: true
add :name, :string
add :discipline_uuid, references(:disciplines, type: :uuid, column: :uuid)
end
end

So we’ve done the same thing with our course in terms of giving it a UUID as its PK, but we’ve also let it know that it has a discipline_uuid column because it belongs to that model. Because it’s not an id and couldn’t infer the name of the column, we pass that information through.

  • The Discipline changeset remains the same; the schema changes only slightly:
# schema for Disciplineschema "disciplines" do
field(:name, :string)
has_many(:courses, Course)
end

We just add that the Discipline has many Courses and make no mention of UUIDs.

  • The Course schema and changeset are responsible for denoting the UUIDs:
schema "courses" do
field(:name, :string)
belongs_to(:discipline, Discipline, foreign_key: discipline_uuid, references: :uuid, primary_key: true)
end
def changeset(course, attrs \\ %{}) do
course
|> cast(attrs, [:name, :discipline_uuid])
|> validate_required([:name, :discipline_uuid])
|> foreign_key_constraint(:discipline_uuid)
end

There are two things to note in here:

  1. We’ve added in the schema a belongs_to field and passed it several options related to the Discipline. We tell it that the column name on the courses table is discipline_uuid , that it references the column uuid on discipline, and that it is that primary key of that model.
  2. In the changeset, we allow it to pass through discipline_uuid as one of the attributes on Course and pipe it through the foreign_key_constraint so that the Course cannot be created with a Discipline parent.

And it should *just work.*

There are a few variations on how you can set this up, but this was the most straightforward for our needs. Comment with your own thoughts and set ups!

Thanks for reading! Want to work on a mission-driven team that loves the fine art of stock photos and Elixir? We’re hiring!

To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.

Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.

--

--

Meryl Dakin
Flatiron Labs

Dev at Knock.app. @meryldakin on github, LinkedIn, and twitter.