UUIDs for User IDs

Integers By Default 🔢

Phoenix's generators save a ton of time writing boilerplate code. Pow is an Elixir package and Phoenix extension that offers a great way to get user authentication up and running in very little time. But by default Phoenix's generators use auto-incrementing integers for user IDs.

What's Wrong with Integer IDs 🤔?

I've been bitten nastily by integer IDs multiple times in my career.

One time a backup failure caused the counter on user IDs to be reset, and the account records were erased while the data still existed, un-attached. When new accounts got created, those IDs got re-used, and existing data got attached to the new accounts, super bad news. (This is also an argument for using a SQL store with strong relational guarantees, rather than the NoSQL store the company had adopted at the time).

On another occasion, user IDs were being randomly generated, but they were still rather small integers, and the likelihood of a collision was high due to the rate at which new accounts were being created. A simple oversight in the application code was performing an upsert on the newly generated user accounts instead of an insert. The Database would have detected a conflict and thrown an error in the case of an insert on a re-used user ID, but since the app was upserting, re-used user IDs just clobbered old accounts with new credentials. Yikes!

Neither of these problems were caused by integer IDs, but in both cases, using a longer, richer user ID would have massively reduced the likelihood that the bugs would have had negative outcomes.

Switching to UUIDs 👷🏿‍♀️

Let's say you've already gone through the process of generating a new Phoenix app, and you've already followed all the guides from Pow on getting set up, and then you realize that your user IDs are integers. Stop! Don't throw away all of your code and start over. I found myself in the same situation this morning, but thanks to Cam Stuart on GitHub, I got my IDs switched over in no time. Here are the steps.

Change the migration.

Open up priv/repo/migrations/<timestamp>_create_users.exs
Change
  def change do
    create table(:users) do
...
  def change do
    create table(:users, primary_key: false) do
...

Which tells the migration not to create the default auto-incrementing ID column. Then add a line to create your own ID column:

  def change do
    create table(:users, primary_key: false) do
    add :id, :uuid, primary_key: true
...

Change the Model

Next open up lib/boots/users/user.ex and add two module attributes above the "schema" declaration:

  @primary_key {:id, :binary_id, autogenerate: true} # Add this
  @foreign_key_type :binary_id # And add this
  schema "users" do
    ...

Those module attributes tell the schema that it should use UUIDs (here represented as :binary_id) for the ID column, and that it should auto-generate them. It also tells any other schemas that when they're making a foreign key that references this table, they should also use UUIDs.

Update the Test

If you've followed the Pow guide on adding API tokens, you'll have a failing test now. Open up test/your_app_web/api_auth_plug_test.exs and change the lines that create a test user with an integer ID:

    user =
      Repo.insert!(%User{id: 1, email: "test@example.com"})

To use a UUID instead:

    user =
      Repo.insert!(%User{id: "e2c54c31-e574-4c9f-8672-dad23449d4cf", email: "test@example.com"})

Change your Generators

Now that you've fixed your users, let's make it so that any new models you generate will use UUIDs by default. Open up config/config.exs and add the following lines to the end of the file:

config :your_app, :generators,
  migration: true,
  binary_id: true,
  sample_binary_id: "11111111-1111-1111-1111-111111111111"

🚨 Don't forget to change :your_app to the actual name of your app!

This should tell phoenix that whenever it's generating a new model, it should use binary IDs for them.

All Done!

Congratulations! Your app is now a little more robust 👏. It's worth noting that there has been some discussion about whether using random UUIDs significantly hurts Postgres's performance by causing it to do more random seeks. After my research, the evidence against UUIDs has appeared weak enough to me that I'd rather have the safety than be concerned about possible performance loss. I'd love to hear from you if you have more information on this, though!

Leave a Reply

Your email address will not be published. Required fields are marked *