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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Pushing arithmetic into the database with F expressions and Sum avoids pulling every row into Python.
- 2Coalesce turns a NULL aggregate over zero related rows into a safe default like 0.
- 3Comparing computed values against stored ones lets you bulk_update only the rows that actually changed.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
ruby
class LogAggregator BUCKET_FORMAT = "%Y-%m-%dT%H:%M" def initialize(entries)
Bucketing log entries by the minute in Ruby
aggregation
hashing
enumerable
Intermediate
5 steps
ruby
class WeeklySignupsReport DEFAULT_WEEKS = 12 def initialize(weeks: DEFAULT_WEEKS, source: User.all)
Building a weekly signups report in Rails
service object
aggregation
group by
Intermediate
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/recomputing-order-totals-in-django-explained-python-4138/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.