In a Django model, how does GeneratedField compute a line item's total, and what do its expression, output_field and db_persist arguments control?
answer
- the database does the arithmetic
- three required keyword arguments
- stored versus virtual
- refreshed after save since 6.0
basics
~20 sGeneratedField declares a GENERATED ALWAYS column: expression is the database-side formula, output_field sets the column's type, and db_persist chooses a stored column (True) or a virtual one computed on read (False). Django never writes it.
solid answer
~40 s`total = models.GeneratedField(expression=F("quantity") * F("unit_price"), output_field=models.DecimalField(max_digits=12, decimal_places=2), db_persist=True)` makes the database compute `total` on every insert and update. `expression` must be deterministic and reference only columns of the same table, not other generated fields or related models. `output_field` gives the column its type and lookups. `db_persist=True` stores the value; `False` computes it on read, which PostgreSQL supports only from version 18 (Django 6.1). The field is never editable, cannot take `default` or `db_default`, and Django omits it from INSERT and UPDATE statements. Since Django 6.0 it is refreshed after `save()` via `RETURNING` on SQLite, PostgreSQL and Oracle; on MySQL and MariaDB it is deferred and loaded on first access.
code
python · 19 linesfrom django.db import models
from django.db.models import F
class LineItem(models.Model):
order = models.ForeignKey("orders.Order", on_delete=models.CASCADE)
line_no = models.PositiveSmallIntegerField()
quantity = models.PositiveIntegerField()
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
total = models.GeneratedField(
expression=F("quantity") * F("unit_price"),
output_field=models.DecimalField(max_digits=12, decimal_places=2),
db_persist=True,
)
line = LineItem.objects.create(order=order, line_no=1, quantity=3, unit_price="4.50")
line.total # Decimal('13.50') on PostgreSQL with Django 6.x, via RETURNING
LineItem.objects.filter(total__gte=100).order_by("-total")go deeper
Recall that GeneratedField is computed by the database from other columns in the same row, and that you never assign it yourself.
Explain the three required arguments, stored versus virtual columns, and why the expression cannot join to other tables.
Know the backend differences, stored-only PostgreSQL before 18 and RETURNING refresh versus deferred loading, and when a Python property is the better tool.
Decide which derived values belong in the schema, where every writer shares them, and which stay in application code where rules change often.
## What a generated column is A **generated column** is a database column whose value the database computes from other columns of the same row, using the SQL `GENERATED ALWAYS AS (...)` syntax. Application code never writes it. Django 5.0 added **`GeneratedField`** to declare one on a model, so the formula lives in the schema and every writer — the ORM, raw SQL, another service — gets the same value. For an order line, the classic case is `total = quantity × unit_price`. ## The three arguments `GeneratedField(*, expression, output_field, db_persist, **kwargs)` takes all three as **required keyword arguments**: | Argument | What it controls | Rules | |---|---|---| | `expression` | the formula the database evaluates | an ORM expression such as `F("quantity") * F("unit_price")`; deterministic; only fields of the same table; no other generated fields | | `output_field` | the column's data type, and which lookups work on it | a field *instance*, such as `DecimalField(max_digits=12, decimal_places=2)` | | `db_persist` | stored or virtual | `True` = stored, computed on write and kept on disk; `False` = virtual, computed on read with no storage | Django checks `db_persist` is exactly `True` or `False` and raises `ValueError` otherwise. The expression is compiled with joins disallowed, which is why `F("order__discount")` cannot appear in it. ## Stored or virtual - **Stored** (`db_persist=True`) — computed on insert and update and saved like a real column; reads are cheap, and on backends that allow it the column can be indexed. Think of it as a materialized value. - **Virtual** (`db_persist=False`) — computed when read and taking no space; closer to a view. Support varies. PostgreSQL before 18 offers only stored generated columns; Django 6.1 added virtual ones on PostgreSQL 18+. Oracle before 23ai (23.7) only supports virtual ones; 6.1 added stored ones on newer Oracle. System checks report `fields.E221` or `fields.E222` when the chosen kind is unsupported, and `fields.E220` when the database has no generated columns at all. Beyond that, databases impose restrictions Django does not validate — PostgreSQL, for example, requires functions in the expression to be `IMMUTABLE` — so test the expression on the real backend. ## How the ORM treats it 1. **Construction** — `GeneratedField` forces `editable=False` and `blank=True`, and raises `ValueError` if you pass `editable=True`, `default` or `db_default`. `null` has no effect — passing it produces check warning `fields.W225` — because nullability follows from the expression and the database. 2. **Writes** — `save()`, `bulk_create()` and `QuerySet.update()` leave the column out of the SQL they send; the database fills it. 3. **After `save()`** — since **Django 6.0** the value is read back with `RETURNING` on SQLite, PostgreSQL and Oracle, so `line.total` is current immediately. MySQL and MariaDB lack `RETURNING`, so Django marks the field **deferred** and loads it on first access. On 5.2 LTS the ORM did not refresh it; code called `refresh_from_db()` to see the new total. 4. **Reads and queries** — it behaves like a column of its `output_field` type: `filter(total__gte=100)`, `order_by("-total")` and aggregates all work. ## Generated column or something else? A line total can be produced in several ways in Django, and interviewers like to hear why you picked one: | Approach | Where it is computed | Filter and sort in SQL | Consistent for raw SQL writers | |---|---|---|---| | `GeneratedField` | the database, on write or read | yes | yes | | `@property` on the model | Python, per instance | no | not applicable | | `annotate(total=F("quantity") * F("unit_price"))` | the database, per query | yes, in that query | not applicable | | ordinary column set in `save()` | Python, on save | yes | no — bulk updates and raw SQL can skip it | The generated column is the only option that is both queryable everywhere and impossible to get out of sync: `QuerySet.update(quantity=5)` changes the total too, because the database recomputes it, while a column maintained in an overridden `save()` would silently go stale on that path. ## When to use it Use a `GeneratedField` when a value is a **pure function of the same row** and must be consistent for every writer: totals, normalised search keys, extracted date parts. Keep Python properties for values that need other tables, the current time, or business rules that change often — changing the expression later is a schema change, not a code change.
- Can a GeneratedField expression use a related model's field, such as F('order__discount')?No. The expression is resolved with joins disallowed and may reference only columns of the same table, not other generated fields. A value that needs another table belongs in a query annotation, a Python property, or a denormalised column maintained by application code.
- What does line.total hold right after save() on MySQL in Django 6.1?It is marked deferred, because MySQL and MariaDB do not support `RETURNING`. The first access to `line.total` runs a query that loads the value the database computed. On SQLite, PostgreSQL and Oracle the value is already current after `save()`.
saying these in an interview costs you the question
- You can assign line.total in Python and save() will store your value.
- db_persist defaults to True, so it can be left out.
- A GeneratedField expression may join to related models through F('order__x').
- After save(), the generated value is always stale until refresh_from_db().
- GeneratedField can take a default to use before the database computes it.