UUIDv7 in Django
Abenezer Belachew · November 11, 2024
6 min read
Alright, I'm not here to tell you that UUIDv7 is the best choice among UUID versions—that's up to your project's needs. But if you're curious about what makes UUIDv7 unique and want to set it up in Django, you're in the right place. UUIDv7 offers timestamp-based ordering, a feature UUIDv4 can't deliver out of the box. For a deeper dive into comparing UUID versions, I've linked some resources at the end of this post.
What is UUIDv7?
-
Like other UUIDs, UUIDv7 is a 128-bit unique identifier used to tag data. You're probably familiar with UUIDv4, the go-to for generating random IDs. Since v4 UUIDs are purely random, the chances of duplicates are tiny—which is great. The downside? UUIDv4 doesn't naturally sort, which can slow things down if you're using it as an index.
-
UUIDv7, on the other hand, is a lexicographically sortable UUID with a built-in timestamp. This allows UUIDs to sort by their creation time, which is perfect for faster lookups and better indexing performance in databases.
UUIDv7 Structure
0190163d-8694-739b-aea5-966c26f8ad91
└─timestamp─┘ │└─┤ │└───rand_b─────┘
ver │var
rand_a
source: Anton Zhiyanov
Bit Size | Description | |
---|---|---|
Timestamp | 48 | Unix timestamp in milliseconds (sortable by generation time) |
Version | 4 | Version (set to 7 for UUIDv7) |
Random Part A | 12 | Random component (rand_a ), for uniqueness within the same millisecond |
Variant | 2 | Defines the UUID layout as RFC 4122 compliant |
Random Part B | 62 | Additional random component (rand_b ), providing further uniqueness |
UUIDv7 in Python
Here's what a uuidv7 function in python looks like, based on Anton Zhiyanov's post on UUIDv7 in 33 languages:
import os
import time
def uuidv7() -> uuid.UUID:
"""
Generate a UUIDv7.
"""
# Generate 16 random bytes as a base for the UUID
value = bytearray(os.urandom(16))
# Get the current timestamp in milliseconds since the Unix epoch
timestamp = int(time.time() * 1000)
# Insert the timestamp (48 bits) into the UUID
value[0] = (timestamp >> 40) & 0xFF
value[1] = (timestamp >> 32) & 0xFF
value[2] = (timestamp >> 24) & 0xFF
value[3] = (timestamp >> 16) & 0xFF
value[4] = (timestamp >> 8) & 0xFF
value[5] = timestamp & 0xFF
# version and variant
value[6] = (value[6] & 0x0F) | 0x70
value[8] = (value[8] & 0x3F) | 0x80
return uuid.UUID(bytes=bytes(value))
So how would you use this in Django?
Using UUIDv7 in Django
To use UUIDv7 in Django, you can create a custom field that extends Django's UUIDField
and
generates UUIDv7s based on the function above.
Step 1: Create a custom UUID field
import os
import time
import uuid
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.utils import timezone
def uuidv7() -> uuid.UUID:
"""
Generate a UUIDv7.
"""
# random bytes
value = bytearray(os.urandom(16))
# current timestamp in ms
timestamp = int(time.time() * 1000)
# timestamp
value[0] = (timestamp >> 40) & 0xFF
value[1] = (timestamp >> 32) & 0xFF
value[2] = (timestamp >> 24) & 0xFF
value[3] = (timestamp >> 16) & 0xFF
value[4] = (timestamp >> 8) & 0xFF
value[5] = timestamp & 0xFF
# version and variant
value[6] = (value[6] & 0x0F) | 0x70
value[8] = (value[8] & 0x3F) | 0x80
return uuid.UUID(bytes=bytes(value))
class UUIDField(models.UUIDField):
"""
A custom UUID field that generates different UUID versions.
"""
def __init__(
self,
primary_key: bool = True,
version: int | None = None,
editable: bool = False,
*args,
**kwargs
):
if version:
if version == 2:
raise ValidationError("UUID version 2 is not supported.")
if version < 1 or version > 7:
raise ValidationError("UUID version must be between 1 and 7.")
version_map = {
1: uuid.uuid1,
3: uuid.uuid3,
4: uuid.uuid4,
5: uuid.uuid5,
7: uuidv7,
}
kwargs.setdefault("default", version_map[version])
else:
kwargs.setdefault("default", uuid.uuid4)
kwargs.setdefault("editable", editable)
kwargs.setdefault("primary_key", primary_key)
super().__init__(*args, **kwargs)
- This custom
UUIDField
accepts an optional version argument. If you pass version=7, it will use the uuidv7 function; otherwise, it defaults to uuid.uuid4. You can customize the default version or extend the dictionary to support other versions as needed.
Step 2: Use the custom UUID field in your models
- You can use this custom field in your models like this:
from django.db import models
from app.fields import UUIDField
class MyModel(models.Model):
id = UUIDField(primary_key=True, version=7)
name = models.CharField(max_length=100)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
And that's it! You now have a Django model that uses UUIDv7 as the primary key. Depending on your use case, you can use it as an index for faster lookups or for sorting by creation time.
Testing UUIDv7 in the Django Shell
- After running migrations and creating some
MyModel
instances, you can check the generated UUIDs in the Django shell:
>>> from app.models import MyModel
>>> ids = MyModel.objects.all().values_list('id', flat=True)
>>> for id in ids:
... print(id)
...
UUID('01927d8c-1645-7ab9-86b6-d351a982a38f')
UUID('0192628e-ef00-7c4f-bdd6-811ff48f0e9b')
UUID('01927d8b-de00-7e2a-9cf7-b86dcb6d7c44')
UUID('01927db9-1b51-7844-94a7-349f372bdc00')
UUID('01927dee-f946-79dc-aac0-ec2ebbd740c4')
UUID('01927e08-c241-7b1b-bf0a-a63413abbdf2')
UUID('0192628a-de38-75f8-8b18-c43496533ad8')
You can check the validity of your v7 UUIDs on uuid7.com.
Tip
- A quick way to identify if a UUID might be version 7 is by looking at the position of the version field. In a UUIDv7, the version number (shown as 7) is located in the 13th character of the UUID string.
Here are some example UUIDv7s with the version 7
highlighted:
- 01927d8c-1645-7ab9-86b6-d351a982a38f
- 0192628e-ef00-7c4f-bdd6-811ff48f0e9b
- 01927d8b-de00-7e2a-9cf7-b86dcb6d7c44
- 01927db9-1b51-7844-94a7-349f372bdc00
In each case, the 7 at this position indicates a UUIDv7.
Conclusion
- Extending Django's UUIDField gives you the flexibility to support different UUID versions, whether it's for legacy compatibility or future-proofing.
- For most cases, UUIDv4 works perfectly fine. But if you need sortable, time-based IDs, UUIDv7 is a great option to consider.
- Congrats! You're now equipped to tackle your (imaginary or real) scaling challenges with UUIDv7 in Django.
Resources
UUIDv7-Specific Resources:
General UUID and ULID Comparisons:
- Goodbye integers. Hello UUIDv7!
- Identity Crisis: Sequence v. UUID as Primary Key
- Universally Unique Lexicographically Sortable Identifier (ULID)
- Beyond UUIDs: Exploring the Advantages of ULIDs in Modern Software Systems
Additional Reading:
🚏️