Models & the ORM

Django Fundamentals — Models & the ORM
Django Fundamentals
Course 1 · Chapter 5 · Models & the ORM

🗃️ Models & the ORM

A FastAPI project typically bolts on SQLAlchemy (or another ORM) as a separate decision. Django's ORM ships with the framework itself — one more "batteries included" default from Chapter 1. This chapter defines a database table as a Python class, migrates it into existence, and queries it through Django's QuerySet API.

Defining a Model

A model is a Python class whose attributes describe the table's columns — Django infers the SQL types, the column names, and generates the migration for you:

# catalog/models.py from django.db import models class Book(models.Model): title = models.CharField(max_length=200) description = models.TextField(blank=True) price = models.DecimalField(max_digits=6, decimal_places=2) published_date = models.DateField(null=True, blank=True) in_stock = models.BooleanField(default=True) def __str__(self): return self.title

Field Types

CharField(short text, requires max_length), TextField (unlimited text), IntegerField, DecimalField, DateField, BooleanField — each maps to an appropriate SQL column type.

blank vs null

null=True allows NULL at the database level; blank=True allows an empty value in forms/validation — the two are independent and commonly both set together for optional fields.

__str__

Not required, but without it, the admin site and shell both show an unhelpful Book object (1) instead of the book's actual title.

Implicit id

Django adds an auto-incrementing primary key field automatically unless you define your own — no need to declare it yourself.

🔀 Migrations

Django's migrations are auto-generated by diffing your models against the last known schema state — you rarely hand-write the SQL yourself, which is a real difference from writing raw migration files by hand:

python manage.py makemigrations catalog # Migrations for 'catalog': # catalog/migrations/0001_initial.py # - Create model Book python manage.py migrate # Applying catalog.0001_initial... OK

makemigrations

Compares your current models against the migration history and writes a new migration file describing the difference — it does not touch the database.

migrate

Actually applies pending migration files to the database — the step that runs the real CREATE TABLE/ALTER TABLE SQL.

🔍 The QuerySet API

Every model gets a default .objects manager — the entry point for every query, returning a QuerySet that reads much like a chained, English-ish sentence:

# All books Book.objects.all() # Filtered — WHERE in_stock = TRUE Book.objects.filter(in_stock=True) # Chained filters — WHERE in_stock = TRUE AND price < 20 Book.objects.filter(in_stock=True).filter(price__lt=20) # Ordering — ORDER BY title ASC Book.objects.order_by("title") # A single row, or an exception if zero or more than one match Book.objects.get(pk=1) # Excluding rows Book.objects.exclude(price=0)

FastAPI + SQLAlchemy vs Django's QuerySet

FastAPI + SQLAlchemy Session
result = session.execute( select(Book).where(Book.in_stock == True) .order_by(Book.title) ) books = result.scalars().all()
Django QuerySet
books = Book.objects.filter(in_stock=True).order_by("title")

QuerySets Are Lazy

Writing Book.objects.filter(...) does not hit the database — a QuerySet only actually runs its query once it's evaluated: iterated over, sliced, or converted to a list/bool:

qs = Book.objects.filter(in_stock=True) # no query yet qs = qs.order_by("title") # still no query — just building up the query for book in qs: # NOW the SQL actually runs print(book.title)

This is why chaining .filter().exclude().order_by() across several lines is cheap — Django combines everything into a single SQL query at the moment it's finally evaluated, rather than running a separate query per method call.

Automatic SQL Injection Protection

Every value passed into the QuerySet API is parameterized automatically — the same defense the SQL Injection course spent a full chapter teaching how to do by hand:

# Completely safe — Django parameterizes `title` regardless of its content Book.objects.filter(title=user_supplied_title) # Even raw() queries are parameterized IF you use placeholders correctly: Book.objects.raw("SELECT * FROM catalog_book WHERE title = %s", [user_supplied_title]) # ❌ Still dangerous — string-formatting a raw query bypasses the protection entirely, # exactly the vulnerability pattern from the SQLi course: # Book.objects.raw(f"SELECT * FROM catalog_book WHERE title = '{user_supplied_title}'")

💻 Coding Challenges

Challenge 1: Define and Migrate a Model

Define an Author model with name (CharField), bio (TextField, optional), and birth_year (IntegerField, optional), add a proper __str__, then write the two commands you'd run to create and apply its migration.

Goal: Practice choosing appropriate field types and the makemigrations/migrate workflow.

→ Solution

Challenge 2: Write Five QuerySet Queries

Against the Book model from this chapter, write QuerySet expressions for: all in-stock books ordered by price ascending, books under $15, the single book with pk=3, all books excluding out-of-stock ones, and the count of all books.

Goal: Build fluency with filter, exclude, order_by, get, and count.

→ Solution

Challenge 3: CRUD in the Django Shell

Using python manage.py shell, write the Python statements to create a new Book, fetch it back with .get(), update its price and save it, then delete it — all through the ORM, no raw SQL.

Goal: Practice the full create/read/update/delete cycle through the ORM's object-oriented API.

→ Solution

⚠️ Gotcha: .get() Raises When It Doesn't Return Exactly One Row

Book.objects.get(title="Dune") looks like it should behave like a dictionary's .get() — returning None if nothing matches. It does not: zero matching rows raises Book.DoesNotExist, and more than one matching row raises Book.MultipleObjectsReturned. Use .get() only when you're certain exactly one row should match (typically by primary key), and reach for .filter(...).first() — which returns None gracefully instead of raising — whenever a missing row is a normal, expected outcome rather than a bug.

🎯 What's Next

A single model can now be created, queried, and migrated — the next chapter covers what happens when models need to reference each other: Model Relationships, ForeignKey and ManyToMany fields, and the N+1 query problem this course has run into before, seen here from the ORM's side.