Postgres UUID Primary Keys in Phoenix
17 Jan 2016I'm at the begining stages of converting a Rails IoT project to Phoenix. Initially I plan to replicate my temperature dashboard, which has the below graph on it.
The green line is the air temperature in my ceiling space, I have a metal roof which is why it heats up to over 50C on some days. Something needs to be done about the heat as it has a massive flow on effect throughout the house in summer. That's another post though.
So far I've created a blank Phoenix app and begun to setup a few models to represent my existing Rails tables so that I can pull the data which powers the dashboard. I immediately encountered an issue as I'm using UUID primay keys everywhere.
In Rails I would create my tables this way:
As far as I'm aware there is no standard way to default the primary key to being :uuid
in Rails, and as a result I'd sometimes forget. The same seems to be the case for Ecto migrations too.
If you get the following error when running the above migration with mix ecto.migrate
Then you need to add the UUID extension to your database from psql like this:
Or you can create an extension to do it for you, just make sure your Postgres user has the appropriate rights, and that this migration runs first.
To then get Ecto in Phoenix using database generated UUIDs isn't straight forward as it doesn't support this, only client side generation is. After a bit of research I've found you can get database generated primary keys used, but it's a bit of a hack.
Reading the Ecto.Schema docs shows how straight forward is it to switch the default primary and foreign key types to :uuid
. The problem is that this line in MyApp.Web.Model
will cause Ecto to generate the primary_key client side and send that in the insert
statements:
If I then run Repo.insert %Reading{}
from iex -S mix
I get the following debug output, note the sql generated includes the UUID being set.
In particular it's the autogenerate
option causing this, if this was of type :id
instead of :binary_id
Ecto would let the database generate the ID. You can see that stated in the code Ecto is generating the UUID. So what to do about this, you can remove the autogenerate: true
, but that doesn't stop Ecto sending the id
field, it's just now nil
which causes a constraint error:
The trick is to use before_insert from Ecto.Model.CallBacks to remove the :id
field from the changeset before the query statement is generated. MyApp.Web.Model
can be updated like this:
The read_after_writes
option will not trigger a select but simply means the database generated id is read from the return response. Now when I insert a new record the :id
field is not being set, a RETURNING
statement is included and the returned model has a primary key.
The convention is to let the client generate the UUID as by their very nature it will be unique, but at this stage I'm going to stick with how the system I'm replacing works. It's not a great idea to immediately go against convention when learning something new, but this hack is quite self contained, and I sure did learn heaps trying to figure all this out.