python 55 lines · 9 steps

Recomputing order totals in Django

A custom QuerySet annotates each order's total from its line items in the database, and a manager syncs stored totals in bulk.

Explained by highlit
1from django.db import models
2from django.db.models import F, Sum, DecimalField
3from django.db.models.functions import Coalesce
4 
5 
6class OrderQuerySet(models.QuerySet):
7 def with_computed_total(self):
8 return self.annotate(
9 computed_total=Coalesce(
10 Sum(
11 F("items__quantity") * F("items__unit_price"),
12 output_field=DecimalField(max_digits=12, decimal_places=2),
13 ),
14 0,
15 output_field=DecimalField(max_digits=12, decimal_places=2),
16 )
17 )
18 
19 
20class OrderManager(models.Manager.from_queryset(OrderQuerySet)):
21 def refresh_totals(self, *order_ids):
22 qs = self.with_computed_total()
23 if order_ids:
24 qs = qs.filter(pk__in=order_ids)
25 
26 updated = []
27 for order in qs:
28 if order.total != order.computed_total:
29 order.total = order.computed_total
30 updated.append(order)
31 
32 if updated:
33 self.bulk_update(updated, ["total"])
34 return len(updated)
35 
36 
37class Order(models.Model):
38 customer = models.ForeignKey("Customer", on_delete=models.CASCADE)
39 total = models.DecimalField(max_digits=12, decimal_places=2, default=0)
40 updated_at = models.DateTimeField(auto_now=True)
41 
42 objects = OrderManager()
43 
44 def sync_total(self):
45 self.total = (
46 self.items.aggregate(
47 value=Coalesce(
48 Sum(F("quantity") * F("unit_price")),
49 0,
50 output_field=DecimalField(max_digits=12, decimal_places=2),
51 )
52 )["value"]
53 )
54 self.save(update_fields=["total", "updated_at"])
55 return self.total
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Pushing arithmetic into the database with F expressions and Sum avoids pulling every row into Python.
  2. 2Coalesce turns a NULL aggregate over zero related rows into a safe default like 0.
  3. 3Comparing computed values against stored ones lets you bulk_update only the rows that actually changed.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Recomputing order totals in Django — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code