skip to content

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?

level: middleimportance: should knowfreq 35%

answer

  1. the database does the arithmetic
  2. three required keyword arguments
  3. stored versus virtual
  4. refreshed after save since 6.0

basics

~20 s

GeneratedField 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 lines
python
from 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

for a junior

Recall that GeneratedField is computed by the database from other columns in the same row, and that you never assign it yourself.

for a middle

Explain the three required arguments, stored versus virtual columns, and why the expression cannot join to other tables.

for a senior

Know the backend differences, stored-only PostgreSQL before 18 and RETURNING refresh versus deferred loading, and when a Python property is the better tool.

for a principal

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.