How to Use UUIDv7 in Rails for Primary Keys
Using UUIDs for primary keys offers many benefits, but there are some downsides to consider. The most widely-used UUIDv4 is fully random, which is ideal for minimizing the risk of collision. However, random IDs as primary keys do not index and sort efficiently, leading to index bloat and performance issues.
There is a new standard that addresses this problem by including a UNIX timestamp1 in the initial bits: UUIDv72.
10.times { puts UUID7.generate }
# 018c2b95-b764-7615-a924-cc5b910ed1e5
# 018c2b95-b764-713f-aff1-e2668182bc68
# 018c2b95-b764-7b43-bec8-3c204a04433a
# 018c2b95-b764-7521-b1de-0638499f2f75
# 018c2b95-b764-7380-895d-4416494f9a05
# 018c2b95-b764-7cb1-a91f-24e598a4bc36
# 018c2b95-b764-7218-a1d1-fa20a475d6b0
# 018c2b95-b764-7eab-9e08-d0b321a69240
# 018c2b95-b764-7d25-a033-845c81610339
# 018c2b95-b764-7143-af66-0a849883f9d0
How to set up UUIDv7 in Rails
As of this writing, selecting id: :uuid
in Rails defaults to using UUIDv4 for primary keys, with the generation deferred to Postgres.
While Postgres does not currently offer a built-in function for generating UUIDv7, we can still use the uuid
type to store values. One advantage of UUIDs over sequential IDs is that they can be generated anywhere — in this example, we’ll use our Rails backend.
First, let’s set up our Rails app to use UUID as the primary key. We’re going to enable pgcrypto
. This isn’t strictly necessary, but Rails will set up gen_random_uuid()
as the default when you set id: :uuid
.
rails g migration EnablePgcrypto
Here’s the migration:
class EnablePgcrypto < ActiveRecord::Migration[7.1]
def change
enable_extension 'pgcrypto'
end
end
Then, add the following to config/application.rb
:
config.generators do |generate|
generate.orm :active_record, primary_key_type: :uuid
end
From now on, Rails will use the UUID type for primary keys for new models created with rails generate model
.
At this point, we’re all set up, but we’re still using UUIDv4. Let’s change that:
bundle add uuid7
To use UUIDv7 for our primary keys, we have to create them before they are saved to the database. This can be achieved with a before_create
callback in the ApplicationRecord
:
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
before_create :generate_uuid_v7
private
def generate_uuid_v7
return if self.class.attribute_types['id'].type != :uuid
self.id ||= UUID7.generate
end
end
That’s it! From now on, all new records will have a UUIDv7 primary key, without affecting the existing tables that may use sequential (integer) IDs.
Further reading
- Choosing a Postgres primary key
- New UUID Formats
- Goodbye integers. Hello UUIDv7!
- Identity Crisis: Sequence v. UUID as Primary Key
- @samokhvalov: How to use UUID
- A brief history of the UUID
Also, check out BasedUUID.
-
The trade-off, compared to v4, is a higher risk of collision, so you’ll have to keep that in mind. Another potential downside is that UUIDv7 exposes the timestamp of record creation. ↩︎
-
UUIDv7 is now recommended over v6. ↩︎