When using SQLite as our Ruby on Rails database, you might wonder how to use something like UUIDs or ULIDs as primary keys.
I got this question on Twitter, and it merits its own post to answer:
Any resources about using UUIDs (or ULIDs) as primary keys in Rails? I’ve gotten used to that with PostgreSQL but it seems a little wonky with SQLite3.
Let’s start with how we configure Rails to use a custom primary key type. Paweł Urbanek has an excellent blog post laying out the steps for using Postgres’ native UUID data type as the primary key for tables. You should give the entire article a read, but for our purposes the two key details are:
- how to set the primary key type in a migration, and
- how to configure Rails to use UUIDs as primary keys for generators
In a migration, we simply pass id: :uuid
as an option to the create_table
method:
create_table :comments, id: :uuid do |t| t.string :content t.uuid :user_id t.timestampsend
In order to have all generated migrations set this option automatically, we can configure Rails:
# config/initializers/generators.rbRails.application.config.generators do |g| g.orm :active_record, primary_key_type: :uuidend
These are the foundational changes you will need to make to your Rails application to enable custom primary keys. How could we do this in SQLite though?
To be honest, custom primary keys weren’t possible when using SQLite with Rails up to today. In order to support this feature, I opened a pull request to add support for RETURNING
non-id
columns for SQLite. And, luckily for all of us SQLite lovers, it has been merged! This means that if you use the main
Rails branch for your application, you can get access to custom primary keys today. So, let’s dig into how.
Personally, I prefer ULIDs to UUIDs, since they are shorter, easier to select and copy, and sortable. Plus, in SQLite-land Alex Garcia’s sqlite-ulid
extension is available as a Ruby gem.
As detailed in a past post on loading extensions, we can extend Rails’ database adapter to support loading extensions. By loading the sqlite-ulid
extension, we add the ulid()
and ulid_bytes()
functions to our SQLite database. We can then use one of these functions for our custom primary keys.
Since the SQLite adapter doesn’t support a ulid
data-type, we need to define our custom primary keys somewhat differently. Instead of passing the id: :ulid
option to the create_table
command, we should instead pass id: false
. This will disable Rails’ default primary key mechanism for your database engine. We can then define the t.primary_key
macro to set the details of our custom primary key. In our case, using ULIDs, we could define the custom primary key like so:
create_table :posts, force: true, id: false do |t| t.primary_key :id, :string, default: -> { "ULID()" }end
Running this migration will create a table with a ULID primary key:
CREATE TABLE "posts" ( "id" varchar DEFAULT (ULID()) NOT NULL PRIMARY KEY)
Now, calling Post.create!
will return a model instance like this #<Post id: "01hayj8d41d5e4hx0fdfbvja76">
.
Unfortunately, we can’t setup the Rails generators to auto-create migrations like this, since we don’t have a custom ulid
data-type. But, a bit of manual effort isn’t a bad thing.
Once we have custom primary keys, we need to ensure that our foreign keys are appropriately matched. In order to ensure our foreign keys are correctly bound to our custom primary keys, we need to tweak our migrations minorly:
create_table :comments, force: true, id: false do |t| t.primary_key :id, :string, default: -> { "ULID()" } t.belongs_to :post, null: false, foreign_key: true, type: :stringend
Whatever the data-type we use for our primary keys, we need to set that type for our foreign keys. This is the only thing we need to ensure. Using the belongs_to
or references
method will automatically setup the rest of our foreign key with our custom primary key. A create_table
like the one above will produce the following SQL:
CREATE TABLE "comments" ( "id" varchar DEFAULT (ULID()) NOT NULL PRIMARY KEY, "post_id" varchar NOT NULL, CONSTRAINT "fk_rails_2fd19c0db7" FOREIGN KEY ("post_id") REFERENCES "posts" ("id"))
That’s it. That’s everything you need to know to setup custom primary and foreign keys for your Rails application using SQLite as your database engine.
In the future, I will be investigating how to register a custom ulid
data-type so that we can simplify this setup even further. So, keep your eyes out for that upcoming post.
In the meantime, I hope you enjoyed this exploration into one of the features unlocked by supporting RETURNING
statements with the SQLite adapter.
All posts in this series #
- Part 1 — branch-specific databases
- Part 2 — fine-tuning SQLite configuration
- Part 3 — loading extensions
- Part 4 — setting up
Litestream
- Part 5 — optimizing compilation
- Part 6 — array columns
- Part 7 — local snapshots
- Part 8 — Rails improvements
- Part 9 — performance metrics
- Part 10 — custom primary keys
- Part 11 — more Rails improvements
- Part 12 — table schema and metadata
- Part 13 — prefixed ULID keys
- Part 14 — installing extensions