Django 5.0 Database Computed Columns
django python
Django 5.0 introduced two powerful features: GeneratedField for computed columns and db_default for database-level defaults. Here’s how to use them effectively.
db_default: Database-Level Defaults
Basic Usage
from django.db import models
from django.db.models.functions import Now, Random
class Order(models.Model):
# Database generates timestamp
created_at = models.DateTimeField(db_default=Now())
# Database generates random float
sort_key = models.FloatField(db_default=Random())
# Static default at database level
status = models.CharField(max_length=20, db_default="pending")
Why db_default Over default?
# Python default (runs in application)
created_at = models.DateTimeField(default=timezone.now)
# Database default (runs in database)
created_at = models.DateTimeField(db_default=Now())
| Aspect | Python default | db_default |
|---|---|---|
| Works with bulk_create | No | Yes |
| Works with raw SQL | No | Yes |
| Timezone handling | Python | Database |
| Consistency | Per-process | Database-wide |
Database Functions
from django.db.models import F, Value
from django.db.models.functions import Now, Concat, Lower
class Article(models.Model):
title = models.CharField(max_length=200)
# Slug from title (simplified)
slug = models.CharField(
max_length=200,
db_default=Lower(F("title")) # Won't work—F() at creation time
)
# Timestamp
published_at = models.DateTimeField(db_default=Now())
UUID Generation
# PostgreSQL
class Document(models.Model):
uuid = models.UUIDField(
db_default=models.Func(function="gen_random_uuid")
)
GeneratedField: Computed Columns
What Is It
GeneratedField creates columns computed from other columns:
from django.db.models import F, GeneratedField
class Product(models.Model):
price = models.DecimalField(max_digits=10, decimal_places=2)
tax_rate = models.DecimalField(max_digits=5, decimal_places=4)
# Computed by database, stored in database
price_with_tax = GeneratedField(
expression=F("price") * (1 + F("tax_rate")),
output_field=models.DecimalField(max_digits=10, decimal_places=2),
db_persist=True # Stored, not computed on every read
)
db_persist Options
# STORED: Computed on INSERT/UPDATE, stored physically
price_with_tax = GeneratedField(
expression=...,
output_field=...,
db_persist=True
)
# VIRTUAL: Computed on every read (PostgreSQL, MySQL)
price_with_tax = GeneratedField(
expression=...,
output_field=...,
db_persist=False
)
| db_persist | Storage | Computation | Indexable |
|---|---|---|---|
| True | Physical | On write | Yes |
| False | None | On read | No* |
*Virtual columns can be indexed in some databases.
Use Cases
Full Name
class Person(models.Model):
first_name = models.CharField(max_length=100)
last_name = models.CharField(max_length=100)
full_name = GeneratedField(
expression=Concat(
F("first_name"),
Value(" "),
F("last_name")
),
output_field=models.CharField(max_length=201),
db_persist=True
)
Search Vector (PostgreSQL)
from django.contrib.postgres.search import SearchVector
class Article(models.Model):
title = models.CharField(max_length=200)
body = models.TextField()
search_vector = GeneratedField(
expression=SearchVector("title", weight="A") +
SearchVector("body", weight="B"),
output_field=models.GeneratedField(),
db_persist=True
)
class Meta:
indexes = [
GinIndex(fields=["search_vector"])
]
Derived Status
class Subscription(models.Model):
expires_at = models.DateTimeField()
created_at = models.DateTimeField(db_default=Now())
is_active = GeneratedField(
expression=Q(expires_at__gt=Now()),
output_field=models.BooleanField(),
db_persist=False # Virtual—always current
)
Indexing Generated Fields
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
quantity = models.IntegerField()
total_value = GeneratedField(
expression=F("price") * F("quantity"),
output_field=models.DecimalField(max_digits=15, decimal_places=2),
db_persist=True
)
class Meta:
indexes = [
models.Index(fields=["total_value"]),
]
Queries filtering on total_value now use the index.
Migration Example
# Generated migration
class Migration(migrations.Migration):
dependencies = [
('myapp', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='product',
name='price_with_tax',
field=models.GeneratedField(
db_persist=True,
expression=django.db.models.expressions.CombinedExpression(
django.db.models.expressions.F('price'),
'*',
django.db.models.expressions.CombinedExpression(
django.db.models.expressions.Value(1),
'+',
django.db.models.expressions.F('tax_rate')
)
),
output_field=models.DecimalField(decimal_places=2, max_digits=10)
),
),
]
Database Support
| Database | db_default | GeneratedField (Stored) | GeneratedField (Virtual) |
|---|---|---|---|
| PostgreSQL | ✅ | ✅ | ✅ |
| MySQL | ✅ | ✅ | ✅ |
| SQLite | ✅ | ✅ | ❌ |
| Oracle | ✅ | ✅ | ✅ |
Best Practices
1. Use for Truly Derived Data
# Good: Derived from other fields
total = GeneratedField(
expression=F("quantity") * F("unit_price"),
...
)
# Bad: Could just be a property
@property
def total(self):
return self.quantity * self.unit_price
Use GeneratedField when you need to query/index the value.
2. Consider Update Cost
# Stored: Updates on every write
# If price or tax_rate changes frequently, consider virtual
price_with_tax = GeneratedField(..., db_persist=False)
3. Combine with Indexes
class Meta:
indexes = [
models.Index(fields=["generated_field_name"])
]
If you’re querying on generated fields, index them.
Final Thoughts
db_default and GeneratedField push logic to the database where it belongs for certain use cases. They enable:
- Consistent defaults across all insert paths
- Derived data without denormalization maintenance
- Query optimization through indexing computed values
Use them when your computed data needs to be queried, not just displayed.
Let the database do the work it’s good at.