Using UUIDs with Gorm and Postgres
In this post I would like to document my adventures of trying to set up a small Go project using
- a Postgres database
- with UUID primary keys for my models,
gormas an ORM, and
gooseas a migration tool.
Disclaimer: I don’t have vast experience in this topic, so take these tips with a pinch of salt.
- Migrating the database with Goose
UUID stands for universally unique identifier (the term GUID refers to the same thing, typically used in the Microsoft world). I wanted to use UUID for multiple reasons.
# Advantages of the UUIDs
- They are unique, which is really handy when it comes to an identifier… They are unique across all tables, all your databases, all your systems. They are practically unique in the universe - theoretically, there could be a collision, however, to get a grasp of how unlikely that is, have a look at this great post: Are UUIDs really unique?. But in short:
“A sample of 3.26 x 10¹⁶ UUIDs has a 99.99% chance of not having any duplicates.”
- They are marginally more secure (not that they should ever be exposed publicly, but still, they practically cannot be guessed.)
- You can create them in the application code (no need to reach out to the DB) if you want to.
# Disatvantages of UUIDs
- Storage: UUIDs are 128-bit values as opposed to either 32 or 62 bit integers. I can imagine this being a significant drawback in some cases.
- Performance: on the same note as above, these are larger values which can adversly affect performance
- Less human readable, thus less convenient to use them during debugging
- No natural ordering, again not great for debugging and for certain types of pagination techniques (this one on the wordpress blog cannot be used, for instance).
# Postgres UUIDs
In Postgres, you can generate UUIDs with the
As an interesting aside, the Postgres documentation mentions that the first version of this function used the MAC address of the host machine, plus a timestamp, which meant it could reveal the identity of the machine and the time the particular record was generated, which is not great from a security perspective.
Another interesting thing is that if you want reproducible UUIDs without any randomness in the generation process, you can use the
v3 of this function with a specified “namespace” (any UUID) and a name. The UUID generated for the same namespace with the same name will always be the same:
So it is best to generate them with the 4th generation family of these functions. Only the 4th generation is truly random (I assume “truly pseudo-random”, unless they solved the random number generation problem since I’ve last checked).
Gorm is an ORM library for Go, as in, it handles the object-relational mapping problem of converting rows in your SQL database to objects in your object-oriented application code. I’m not very experienced with Gorm, but so far it definitely seems like an awesome tool which greatly facilitates development.
One could say that you might not need any ORM for your Go project, for instance, if your app has very simple models and relationships, or if performance and throughput are critical for your service. In these cases, using a raw database driver and the
sqlpackage might be best (http://go-database-sql.org/index.html).
To define an ORM model, you can use the
gorm.Model struct embedded in your struct:
gorm.Model part in the struct definition add
DeletedAt fields to your model (have a look at the source here).
However, if you want to use UUIDs, you have to specify the type of the column
gorm, by manually typing it into your struct definition. This means you have to remove the
gorm.Model part, as that already contains the ID part. This has bitten me and I have a good hour or two debugging my application before I realised this. In this case, the
User struct might look like this with the customisation of the ID field:
In your docker-compose, you can define a Postgres database like this:
Note that I am using a Postgres Docker image from
frodenas on Dockerhub, because it’s trivial to install an extension with it - just specify it as an environment variable called
POSTGRES_EXTENSIONS in the compose file, and it will be installed when you first build the image.
However, you could use the latest official Postgres image at dockerhub.com/postgres as well, and create a tiny SQL script with the contents
create extension if not exists "uuid-ossp";
in a file in the
/docker-entrypoint-initdb.d directory to achieve the same results.
# Migrating the database with Goose
users table with the appropriate schema can be done manually inside the docker container. However, for developer sanity, you might want to use a migration tool to automate this.
There is a nifty tool called
goose for that - it generates the skeleton sql files that you use for up- and down migrations, and it runs them according to your needs.
To create an empty migration in the
migrations directory, you can run the following:
To check the status of your DB, run
In the newly created file, you can define your table schema:
Finally, to actually run the migrations, run
And if you jump on to the docker container running Postgres, you should see the tables that
goose table is used by the migration tool to determine the current state of the DB. Its schema looks like this:
I think that’s it, kind folks! This wasn’t too much fun perhaps, but maybe it helps some random people to set up UUIDs with Gorm + Goose + Postgres.