A few days ago I started using Elixir and the Phoenix Framework (v 0.12.0) with a Postgres database. I'm trying to create a table which has a UUID primary key, which I prefer over the sequential default.
After using mix phoenix.gen.html
to generate the model and migration files and following the other steps in the Phoenix docs, I have changed
def model do
quote do
use Ecto.Model
end
end
in web.ex
to
def model do
quote do
use Ecto.Model
@primary_key {:id, :uuid, []}
@foreign_key_type :uuid
end
end
as is mentioned in the Ecto docs. I have also changed the migration to
create table(:tblname, primary_key: false) do
add :id, :uuid, primary_key: true
[other columns]
end
Unfortunately, when I try to add an entry to the table from the auto-generated form, I get an error because the id
is null. If I manually add an id
-column to the model, I receive an error that the column already exists. If I neglect to set primary_key
to false in table/2
and remove the id
column, the table is generated with a sequential id
-column.
Do I need to manually set the id
in the changeset, or have I made an error in setting up my app to use UUIDs? Thanks in advance
EDIT: I have updated this answer to Ecto v2.0. You can read the previous answer at the end.
Handling UUIDs in Ecto has become much more straight-forward since the original answer. Ecto has two types of IDs: :id
and :binary_id
. The first is an integer ID as we know from databases, the second is database specific binary. For Postgres, it is a UUID.
To have UUID as primary keys, first specify them in your migration:
create table(:posts, primary_key: false) do
add :id, :binary_id, primary_key: true
end
Then in your model module (outside the schema
block):
@primary_key {:id, :binary_id, autogenerate: true}
When you specify the :autogenerate
option for :binary_id
, Ecto will guarantee that either the adapter or the database will generate it for you. However, you can still generate it manually if you prefer. Btw, you could have used :uuid
in your migration and Ecto.UUID
in your schema instead of :binary_id
, the benefit of :binary_id
is that it is portable across databases.
You need to tell your database how to automatically generate the UUID for you. Or you need to generate one from the application side. It depends which one you prefer.
Before we move on, it is important to say that you are using :uuid
that will return binaries instead of a human readable UUIDs. It is very likely you want to use Ecto.UUID
which will format it as a string (aaaa-bbb-ccc-...) and that's what I'll use below.
In your migration, define a default for the field:
add :id, :uuid, primary_key: true, default: fragment("uuid_generate_v4()")
I am assuming you are running on PostgreSQL. You need to install the uuid-ossp extension with CREATE EXTENSION "uuid-ossp"
in pgAdmin or add execute "CREATE EXTENSION \"uuid-ossp\""
in the migration. More information about the UUID generator can be found here.
Back to Ecto, in your model, ask Ecto to read the field from the database after insert/update:
@primary_key {:id, Ecto.UUID, read_after_writes: true}
Now, when you insert, the database will generate a default value and Ecto will read it back.
You will need to define a module that inserts the UUID for you:
defmodule MyApp.UUID do
def put_uuid(changeset) do
Ecto.Changeset.put_change(changeset, :id, Ecto.UUID.generate())
end
end
And use it as a callback:
def model do
quote do
use Ecto.Model
@primary_key {:id, Ecto.UUID, []}
@foreign_key_type Ecto.UUID
before_insert MyApp.UUID, :put_uuid, []
end
end
before_insert
is a callback and it will call the given module at the given function with the given arguments, with a changeset representing what is being inserted being given as first argument.
That should be all. Btw, there is a chance this will be more streamlined in the future. :)