Skip to main content
CodeOath
← All posts

Python65 min total · 18 parts

Django Fundamentals: The ORM, Migrations, and Shipping a Real App

Part 18 of 18 · ~2 min

Common Mistakes Worth Remembering

  • N+1 queries. The single line that causes it never looks any different whether it's running once or five hundred times, which is exactly what makes this the bug ToolShed shipped without anyone catching it in review.
  • A bare .get() standing in for a lookup that might come back empty. get_object_or_404 exists precisely so a missing row becomes a 404 a visitor understands, instead of a stack trace they don't.
  • CASCADE picked out of habit rather than considered per relationship. Loan.borrower needed PROTECT, not CASCADE — the difference between a member's account being deletable and their entire loan history disappearing along with it.
  • A model edited without a matching migration. migrate reporting nothing to do isn't a bug in migrate; it's a missing makemigrations run.
  • Logic that belongs on the model creeping into the template instead — an inline overdue check in tool_card.html instead of Loan.is_overdue, where it can actually be unit tested.
  • A form missing {% csrf_token %}, which renders perfectly and then hands back a 403 with nothing in the error pointing at the actual cause.
  • Middleware stacked without checking what it needs to already exist below it — the specific failure being AuthenticationMiddleware reaching for a session SessionMiddleware hasn't populated yet.
  • A post_save signal treated as guaranteed to fire, when any bulk .update() route to the same change skips it — and auto_now — without warning.

None of these are unique to a website, either. Send ToolShed's due-date reminders from a scheduled script instead of a request, and the exact same shape of bug reappears with nothing web-specific about it at all:

# management/commands/send_overdue_reminders.py
for loan in Loan.objects.filter(status="approved"):   # no select_related — an N+1 waiting to happen
    email_reminder(loan.borrower.email, loan.tool.name)   # one extra query per loan, every single run

Same fix, same reasoning, zero web framework involved in causing it: Loan.objects.select_related("borrower", "tool").filter(status="approved"). If the pattern is recognizable here — no request, no template, no browser anywhere in sight — the model that's actually been built up over these sixteen chapters is a query-shape problem, not a web-page problem that happens to also show up in scripts.