A drop-in Django model field for storing sortable, time-encoded ULIDs as 26-character strings.
Project description
django-ulidfield
A drop-in Django model field for storing sortable, time-encoded ULIDs as 26-character strings.
What are ULIDs?
ULIDs (Universally Unique Lexicographically Sortable Identifiers) are a modern alternative to UUIDs that combine the benefits of both sequential integers and random UUIDs. They consist of:
- 48-bit timestamp (milliseconds since Unix epoch)
- 80-bit randomness
01AN4Z07BY 79KA1307SR9X4MV3
|----------------| |------------------------|
time randomness
48bits 80bits
ULIDs are encoded in base-32 (Crockford's Base32) resulting in 26-character strings that are:
- Sortable by creation time
- URL-safe (no special characters)
- Case-insensitive
- Compatible with UUID storage (128-bit)
Why ULIDs over UUIDs?
As explained in Brandur Leach's article "Identity Crisis: Sequence v. UUID as Primary Key", ULIDs solve several problems with traditional UUID v4:
Problems with Random UUIDs
- Poor database performance: Random UUIDs cause index fragmentation and cache misses
- High WAL overhead: More write-ahead log data due to scattered page updates
- No temporal ordering: Can't sort by creation time
ULID Advantages
- Time-ordered: ULIDs sort naturally by creation time
- Better database performance: Sequential timestamp prefix reduces index fragmentation
- Distributed generation: No single point of failure like auto-incrementing integers
- Opaque to users: Prevents enumeration attacks and business intelligence leakage
- UUID compatible: Can be stored in UUID columns when needed
Installation
pip install django-ulidfield
Or with Poetry:
poetry add django-ulidfield
Usage
Basic Usage
from django.db import models
from django_ulidfield import ULIDField
class Article(models.Model):
id = ULIDField(primary_key=True)
title = models.CharField(max_length=200)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
# ULIDs are automatically generated
article = Article.objects.create(title="Hello World", content="...")
print(article.id) # Output: 01AN4Z07BY79KA1307SR9X4MV3
Non-Primary Key Usage
class Order(models.Model):
id = models.AutoField(primary_key=True)
order_id = ULIDField() # Unique by default
customer_email = models.EmailField()
total = models.DecimalField(max_digits=10, decimal_places=2)
Custom Configuration
class Document(models.Model):
# Allow null values
doc_id = ULIDField(null=True, blank=True)
# Custom default function
tracking_id = ULIDField(default=None, null=True)
# Allow duplicates (not recommended)
reference_id = ULIDField(unique=False)
Field Options
ULIDField inherits from Django's CharField and accepts all the same options, with these defaults:
max_length=26(ULIDs are always 26 characters)unique=True(ULIDs should be unique)editable=False(ULIDs are typically auto-generated)default=generate_ulid(automatically generates new ULIDs)blank=False(ULIDs are required by default)
Validation
The field automatically validates that values are proper ULIDs:
# This will raise a ValidationError
invalid_article = Article(id=\"invalid-ulid\")
invalid_article.full_clean() # ValidationError: 'invalid-ulid' is not a valid ULID
Database Considerations
Index Performance
ULIDs provide better database performance than random UUIDs because:
- The timestamp prefix keeps new insertions clustered together
- Reduces index page splits and cache misses
- Minimizes write-ahead log (WAL) overhead
Storage
- Database storage: 26 characters (can be optimized to 16 bytes in UUID columns)
- Memory/JSON: 26-character string
- URL-safe: Can be used directly in URLs
Migration from UUIDs
If you're migrating from UUIDs, you can:
- Direct replacement (new records only):
# Change this:
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
# To this:
id = ULIDField(primary_key=True)
- Gradual migration (with a new field):
class MyModel(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4) # Keep existing
ulid = ULIDField(null=True, blank=True) # Add new field
Time Extraction
You can extract the timestamp from a ULID:
from ulid import ULID
# Get timestamp from ULID
ulid_obj = ULID.from_str(article.id)
timestamp = ulid_obj.timestamp()
datetime_obj = ulid_obj.datetime()
Development
Setup
git clone https://github.com/your-username/django-ulidfield
cd django-ulidfield
poetry install
poetry run pre-commit install
Running Tests
poetry run pytest
Code Quality
This project uses:
- Ruff for linting and formatting
- pytest for testing
- pre-commit for code quality checks
Requirements
- Python 3.9+
- Django 4.2+
- python-ulid 3.0.0+
License
MIT License - see LICENSE file for details.
Related Resources
- ULID Specification
- "Identity Crisis: Sequence v. UUID as Primary Key" - Deep dive on database identifier strategies
- python-ulid library
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_ulidfield-0.1.0.tar.gz.
File metadata
- Download URL: django_ulidfield-0.1.0.tar.gz
- Upload date:
- Size: 5.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.12.3 Linux/6.11.0-1018-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
890d3143d604ff7f67647bff91482c5e6d8c8deaa77b272da37d9b1ae98f2418
|
|
| MD5 |
2800cf1685cf8b7b36a08a8dbade01e1
|
|
| BLAKE2b-256 |
5f452006f76ed1d09b0a0a5ad9f2fb8823f8da0fa17de9ba8295a390d8150f9e
|
File details
Details for the file django_ulidfield-0.1.0-py3-none-any.whl.
File metadata
- Download URL: django_ulidfield-0.1.0-py3-none-any.whl
- Upload date:
- Size: 5.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.12.3 Linux/6.11.0-1018-azure
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad03f9c8c2677a56da66c8211d2834fa4ef54fe1b453916e9400061a7d173f03
|
|
| MD5 |
eeffed23b1806710b392a7fc32fd7e64
|
|
| BLAKE2b-256 |
e9c39a69c26e00ce1d8f46bc6ccb9153f6e5468fbd4deacfc83001ee09b9948e
|