Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django's GeneratedField by example - DjangoCon ...

Django's GeneratedField by example - DjangoCon US 2025

-- A talk I gave at DjangoCon US 2025

The `GeneratedField` is a new field available in Django 5.0 whose value is computed entirely by the database based on the other fields in the model. In this talk, we will learn together how to use this field through various practical examples.

https://www.paulox.net/2025/09/08/djangocon-us-2025/

Avatar for Paolo Melchiorre

Paolo Melchiorre

September 08, 2025
Tweet

More Decks by Paolo Melchiorre

Other Decks in Technology

Transcript

  1. 2

  2. 3

  3. 🦄 DSF board member 🐍 PSF fellow 󰏢 PyCon Italia

    organizer 🐬 Python Pescara founder 🚀 Djangonaut Space navigator 🧡 Django Girls+ organizers Paolo Melchiorre www.paulox.net © 2022 Bartek Pawlik (CC BY-NC-SA)
  4. 7 — Django “Performance and optimization” “… it will almost

    always be faster to do this work at lower rather than higher levels. That is, the database can typically do things faster than Python can …
  5. • computed by the database • always up-to-date • related

    column changes • No trigger • No Python code 11 Generated Columns GENERATED ALWAYS AS (...) STORED
  6. • SQLite ◦ https://www.sqlite.org/releaselog/3_31_0.html • PostgreSQL ◦ https://www.postgresql.org/docs/12/release-12.html • Oracle

    ◦ https://oracle-base.com/articles/11g/virtual-columns-11gr1 • MySQL ◦ https://dev.mysql.com/doc/refman/5.7/en/mysql-nutshell.html • MariaDB ◦ https://mariadb.com/kb/en/changes-improvements-in-mariadb-102 12 References
  7. 13 from django.db import models from django.contrib.postgres import search class

    Album(models.Model): ... title = models.CharField() search = models.GeneratedField( search.SearchVectorField(), models.F('title') )
  8. 15

  9. 16

  10. 17

  11. 18

  12. 19

  13. 20

  14. 21

  15. • Computed value → from other fields • Database managed

    → no extra code • Automatic updates → always in sync 22 GeneratedField (expression, output_field, db_persist=None, **kwargs)
  16. • Database expression → auto field value • Deterministic only

    → same-table fields • No references → other generated fields • Backend limits → extra restrictions 23 GeneratedField expression
  17. • Output field → required option • Model field →

    instance specified • Defines type → for generated value 24 GeneratedField output_field
  18. • True→ stored column • False→ virtual column • Backend

    support → varies by database 25 GeneratedField db_persist
  19. 28 from django.db.models.functions import Pi, Power, Round class Circle(models.Model): radius

    = models.FloatField() area = models.GeneratedField( expression=Round(Power("radius", 2) * Pi(), precision=2), output_field=models.FloatField(), db_persist=True, ) def __str__(self): return f"{self.radius}²×π={self.area}"
  20. 29 BEGIN; -- -- Create model Circle -- CREATE TABLE

    "samples_circle" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "radius" real NOT NULL, "area" real GENERATED ALWAYS AS ( ROUND((POWER("radius", 2) * PI()), 2) ) STORED ); COMMIT;
  21. 30 >>> Circle.objects.create(radius=3.1415) <Circle: 3.1415²×π=31.0> >>> from django.db import connection

    >>> print(connection.queries[-1]['sql']) INSERT INTO "samples_circle" ("radius") VALUES (3.1415) RETURNING "samples_circle"."id", "samples_circle"."area"
  22. 32 from django.db.models import F, Value class Item(models.Model): price =

    models.DecimalField(max_digits=6, decimal_places=2) quantity = models.PositiveSmallIntegerField(db_default=Value(1)) total_price = models.GeneratedField( expression=F("price") * F("quantity"), output_field=models.DecimalField( max_digits=11, decimal_places=2 ), db_persist=True, ) def __str__(self): return f"{self.price}×{self.quantity}={self.total_price}"
  23. 33 BEGIN; -- -- Create model Item -- CREATE TABLE

    "samples_item" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "price" decimal NOT NULL, "quantity" smallint unsigned DEFAULT 1 NOT NULL CHECK ( "quantity" >= 0 ), "total_price" decimal GENERATED ALWAYS AS ( CAST(("price" * "quantity") AS NUMERIC) ) STORED ); COMMIT;
  24. 36 from django.db.models import Case, Value, When class Order(models.Model): creation

    = models.DateTimeField() payment = models.DateTimeField(null=True) status = models.GeneratedField( expression=Case( When(payment__isnull=False, then=Value("paid")), default=Value("created"), ), output_field=models.TextField(), db_persist=True, ) def __str__(self): return f"[{self.status}] {self.payment or self.creation}"
  25. 37 BEGIN; -- -- Create model Order -- CREATE TABLE

    "samples_order" ( "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "creation" datetime NOT NULL, "payment" datetime NULL, "status" text GENERATED ALWAYS AS ( CASE WHEN "payment" IS NOT NULL THEN 'paid' ELSE 'created' END ) STORED ); COMMIT;
  26. 38 >>> Order.objects.create(creation="2023-01-01 12:00Z") <Order: [created] 2023-01-01 12:00Z> >>> Order.objects.create(

    ... creation="2023-01-02 00:00Z", ... payment="2023-01-03 06:30Z", ... ) <Order: [paid] 2023-01-03 6:30Z>
  27. 39 >>> order = Order.objects.create(creation="2023-01-01 12:00Z") >>> order <Order: [created]

    2023-01-03 06:30Z> >>> order.payment = "2023-01-03 06:30Z" >>> order.save() >>> order <Order: [created] 2023-01-03 06:30Z> >>> order.refresh_from_db() >>> order <Order: [paid] 2023-01-03 06:30:00+00:00>
  28. 40 >>> order = Order(creation="2023-01-01 12:00Z") >>> order Traceback (most

    recent call last): ... AttributeError: Cannot read a generated field from an unsaved model.
  29. 41 class Order(models.Model): ... @property def status_str(self): return getattr(self, 'status',

    'created') def __str__(self): return f"[{self.status_str}] {self.payment or self.creation}"
  30. 43 DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", "HOST": "<my_database_host>",

    "NAME": "<my_database_name>", "PASSWORD": "<my_database_password>", "PORT": "<my_database_port>", "USER": "<my_database_user>", } }
  31. 46 from django.db.models import F class Package(models.Model): name = models.CharField()

    data = models.JSONField() version = models.GeneratedField( expression=F("data__info__version"), output_field=models.CharField(), db_persist=True, ) def __str__(self): return f"{self.name} {self.version}"
  32. 47 BEGIN; -- -- Create model Package -- CREATE TABLE

    "samples_package" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "data" jsonb NOT NULL, "version" varchar GENERATED ALWAYS AS ( ("data" #> '{info, version}') ) STORED ); COMMIT;
  33. 48 >>> import json >>> from urllib.request import urlopen >>>

    pkg = {"name": "django"} >>> url = f"https://pypi.org/pypi/{pkg['name']}/json" >>> with urlopen(url) as r: pkg["data"] = json.loads(r.read()) >>> Package.objects.create(**pkg) <Package: django "5.2.6">
  34. 49 class Package(models.Model): name = models.CharField() data = models.JSONField() @property

    def version(self): return self.data.get("info", {}).get("version", "") def __str__(self): return f"{self.name} {self.version}"
  35. 51 >>> qs = Package.objects.defer("data") >>> qs.annotate(version=F("data__info__version")) SELECT "samples_package"."id", "samples_package"."name",

    ("samples_package"."data" #> '{info,version}') AS "version" FROM "samples_package" ORDER BY "samples_package"."id" DESC
  36. • 1,000 packages ◦ Property -> ~4,000 ms ◦ Annotation

    -> ~400 ms ◦ GeneatedField -> ~1.5 ms • 10,000 packages ◦ Property -> ~40,000 ms ◦ Annotation -> ~4.000 ms ◦ GeneatedField -> ~3 ms 53 ⚡ Benchmark Test loading on my local machine
  37. 54

  38. 56 from django.db.models import Value from django.db.models.functions import Concat class

    Person(models.Model): first_name = models.CharField() last_name = models.CharField() full_name = models.GeneratedField( expression=Concat("first_name", Value(" "), "last_name"), output_field=models.CharField(), db_persist=True, ) def __str__(self): return self.full_name
  39. 57 BEGIN; -- -- Create model Person -- CREATE TABLE

    "samples_person" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "first_name" varchar NOT NULL, "last_name" varchar NOT NULL, "full_name" varchar GENERATED ALWAYS AS (( COALESCE("first_name", '') || COALESCE((COALESCE(' ', '') || COALESCE("last_name", '')), '') )) STORED ); COMMIT;
  40. 59 from django.db.models.functions import JSONObject, Upper, Left class Person(models.Model): first_name

    = models.CharField() last_name = models.CharField() json = models.GeneratedField( expression=JSONObject( forename="first_name", surname=Upper("last_name"), init=Concat(Left("first_name", 1), Left("last_name", 1)), ), output_field=models.CharField(), db_persist=True, ) def __str__(self): return self.json
  41. 60 BEGIN; -- -- Add field json to person --

    ALTER TABLE "samples_person" ADD COLUMN "json" varchar GENERATED ALWAYS AS (JSON_OBJECT( (('forename')::text) VALUE "first_name", (('surname')::text) VALUE UPPER("last_name"), (('init')::text) VALUE ( COALESCE(LEFT("first_name", 1), '') || COALESCE(LEFT("last_name", 1), '')) RETURNING JSONB)) STORED; COMMIT;
  42. 63 from django.contrib.postgres import search class Quote(models.Model): author = models.CharField()

    text = models.TextField() search = models.GeneratedField( expression=search.SearchVector("text", config="english"), output_field=search.SearchVectorField(), db_persist=True, ) def __str__(self): return f"[{self.author}] {self.text}"
  43. 64 >>> quote = {"author": "Plato"} >>> quote["text"] = "Man

    is a being in search of meaning" >>> Quote.objects.create(**quote) <Quote: [Plato] Man is a being in search of meaning> >>> Quote.objects.values_list("search") <QuerySet [("'man':1 'mean':8 'search':6",)]> >>> Quote.objects.filter(search="meanings").first() <Quote: [Plato] Man is a being in search of meaning>
  44. 65 from django.contrib.postgres import search class Quote(models.Model): author = models.CharField()

    text = models.TextField() lang = models.CharField(db_default="english") search = models.GeneratedField( expression=search.SearchVector("text", config=F("lang")), output_field=search.SearchVectorField(), db_persist=True, ) def __str__(self): return f"[{self.author}] {self.text}"
  45. 66 BEGIN; -- -- Create model Quote -- CREATE TABLE

    "samples_quote" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "author" varchar NOT NULL, "text" text NOT NULL, "lang" text DEFAULT 'english' NOT NULL, "search" tsvector GENERATED ALWAYS AS ( to_tsvector("lang"::regconfig, COALESCE("text", '')) ) STORED ); COMMIT;
  46. 67 $ python -m manage migrate samples 0010 Operations to

    perform: Target specific migration: 0010_quote, from samples Running migrations: ... django.db.utils.ProgrammingError: generation expression is not immutable
  47. 68 from django.contrib.postgres.search import SearchVector class Quote(models.Model): … lang =

    models.CharField(max_length=2, db_default="en") search = models.GeneratedField( expression=Case( When(lang="en", then=SearchVector("text", config="english")), When(lang="it", then=SearchVector("text", config="italian")), default=SearchVector("text", config="simple"), ), output_field=SearchVectorField(), db_persist=True, )
  48. 69 CREATE TABLE "samples_cuote" ( "id" bigint NOT NULL PRIMARY

    KEY GENERATED BY DEFAULT AS IDENTITY, "author" varchar NOT NULL, "text" text NOT NULL, "lang" varchar DEFAULT 'en' NOT NULL, "search" tsvector GENERATED ALWAYS AS ( CASE WHEN "lang" = 'en' THEN to_tsvector('english'::regconfig, "text") WHEN "lang" = 'it' THEN to_tsvector('italian'::regconfig, "text") ELSE to_tsvector('simple'::regconfig, COALESCE("text", '')) END ) STORED );
  49. 70 >>> quote = {"author": "Cartesius", "text": "I think therefore

    I am"} >>> cite = Quote.objects.create(**quote) >>> cite.search "'therefor':3 'think':2" >>> quote |= {"text": "Io penso dunque sono", "lang": "it"} >>> cite = Quote.objects.create(**quote) >>> cite.search "'dunqu':3 'pens':2" >>> quote |= {"text": "Ego cogito ergo sum", "lang": "la"} >>> cite = Quote.objects.create(**quote) >>> cite.search "'cogito':2 'ego':1 'ergo':3 'sum':4"
  50. 72 from django.contrib.postgres.fields import DateRangeField class Booking(models.Model): start = models.DateField()

    end = models.DateField() span = models.GeneratedField( expression=DateRangeFunc("start", "end"), output_field=DateRangeField(), db_persist=True, ) def __str__(self): return ( f"{self.span.bounds[0]}{self.span.lower.isoformat()} -" f"> {self.span.upper.isoformat()}{self.span.bounds[1]}" )
  51. 74 BEGIN; -- -- Create model Booking -- CREATE TABLE

    "samples_booking" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "start" date NOT NULL, "end" date NOT NULL, "span" daterange GENERATED ALWAYS AS ( DATERANGE("start", "end") ) STORED ); COMMIT;
  52. 75 >>> Booking.objects.create(start="2023-1-1", end="2023-1-9") <Booking: [2023-01-01 -> 2023-01-09)> >>> from

    datetime import date >>> Booking.objects.filter(span__contains=date(2023, 1, 5)) <QuerySet [<Booking: [2023-01-01 -> 2023-01-09)>]>
  53. 77 # generatedfields/generatedfields/settings.py DATABASES = { "default": { "ENGINE": "django.contrib.gis.db.backends.postgis",

    "HOST": "<my_database_host>", "NAME": "<my_database_name>", "PASSWORD": "<my_database_password>", "PORT": "<my_database_port>", "USER": "<my_database_user>", } }
  54. 78 # generatedfields/generatedfields/settings.py INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions",

    "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.postgres", "django.contrib.gis", "samples", ]
  55. 80 from django.contrib.gis.db.models.functions import GeoHash class City(models.Model): name = models.CharField()

    point = models.PointField() geohash = models.GeneratedField( expression=GeoHash("point"), output_field=models.CharField(), db_persist=True, ) def __str__(self): return f"{self.name} ({self.geohash})"
  56. 81 BEGIN; -- -- Create model City -- CREATE TABLE

    "samples_city" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "point" geometry(POINT, 4326) NOT NULL, "geohash" varchar GENERATED ALWAYS AS ( ST_GeoHash("point") ) STORED); CREATE INDEX "samples_city_point_idx" ON "samples_city" USING GIST ("point"); COMMIT;
  57. 82 >>> city = {"name": "Pescara, IT", "point": "POINT(14.21 42.47)"}

    >>> City.objects.create(**city) <City: Pescara, IT (srd1e77msvzfph6zgfjf)>
  58. 83 from django.contrib.gis.db.models.functions import AsGeoJSON from django.db.models.functions import Cast class

    City(models.Model): name = models.CharField() point = models.PointField() geojson = models.GeneratedField( expression=Cast(AsGeoJSON("point"), models.JSONField()), output_field=models.JSONField(), db_persist=True, ) def __str__(self): return self.geojson
  59. 84 BEGIN; -- -- Create model City -- CREATE TABLE

    "samples_city" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "point" geometry(POINT, 4326) NOT NULL, "geojson" jsonb GENERATED ALWAYS AS ( ST_AsGeoJSON("point", 8, 0)::jsonb ) STORED); CREATE INDEX "samples_city_point_idx" ON "samples_city" USING GIST ("point"); COMMIT;
  60. 85 >>> city = {"name": "Pescara, IT", "point": "POINT(14.21 42.47)"}

    >>> City.objects.create(**city) <City: {'type': 'Point', 'coordinates': [14.21, 42.47]}>
  61. 87 from django.contrib.gis.db import models from django.contrib.gis.db.models.functions import Length class

    Route(models.Model): name = models.CharField() line = models.LineStringField() length = models.GeneratedField( db_persist=True, expression=Length("line"), output_field=models.FloatField(), ) def __str__(self): return f"{self.name} (~{self.length / 1000:.1f} km)"
  62. 88 BEGIN; -- -- Create model Route -- CREATE TABLE

    "samples_route" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "line" geometry(LINESTRING, 4326) NOT NULL, "length" double precision GENERATED ALWAYS AS ( ST_LengthSpheroid( "line", 'SPHEROID["WGS 84", 6378137.0, 298.257223563]' )) STORED); CREATE INDEX "samples_route_line_idx" ON "samples_route" USING GIST ("line"); COMMIT;
  63. 89 >>> name = "90 Mile Straight, AU" >>> line

    = "LINESTRING(123.944 -32.455, 125.484 -32.27)" >>> Route.objects.create(name=name, line=line) <Route: 90 Mile Straight, AU (~146.4 km)>
  64. 91 from django.contrib.gis.db import models from django.contrib.gis.db.models.functions import Area class

    State(models.Model): name = models.CharField() polygon = models.PolygonField() area = models.GeneratedField( db_persist=True, expression=Area("polygon"), output_field=models.FloatField(), ) def __str__(self): return f"{self.name} (~{self.area * 10000:.0f} km²)"
  65. 92 BEGIN; -- -- Create model State -- CREATE TABLE

    "samples_state" ( "id" bigint NOT NULL PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, "name" varchar NOT NULL, "polygon" geometry(POLYGON, 4326) NOT NULL, "area" double precision GENERATED ALWAYS AS ( ST_Area("polygon") ) STORED); CREATE INDEX "samples_state_polygon_8801f79a_id" ON "samples_state" USING GIST ("polygon"); COMMIT;
  66. 93 >>> name = "Colorado, US" >>> poly = "POLYGON((-109

    37, -109 41, -102 41, -102 37, -109 37))" >>> State.objects.create(name=name, polygon=poly) <State: Colorado, US (~280000 km²)>
  67. 94

  68. • Simpler code (no save() override, no signals) • Guaranteed

    consistency at the database level • Can use database features (indexes, queries) 95 ✅ Why use it
  69. • GeneratedField = database-computed values in Django • Makes models

    cleaner & safer • Works across multiple backends • Great for real-world apps 96 👇 Summary
  70. • Organizers • Speakers • Sponsors • Volunteers Grazie /ˈɡrat.t

    ͡ sje/ 97 Thanks © 2024 Bartek Pawlik (CC BY-NC-SA)