Models and ORM Basics
Introduction
Django’s ORM maps Python classes to database tables. You define models.Model subclasses, run migrations, and query with Model.objects. This chapter adds a Post model to blog, explains field types and Meta, configures SQLite (default) and MySQL (production preview), and performs basic CRUD from the shell.
Prerequisites
- Templates and Static Files
- Settings —
DATABASES - Optional: MySQL basics
Define the Post Model
blog/models.py:
Code explanation:
| Field | Maps to |
|---|---|
CharField | VARCHAR with max_length |
SlugField | URL-safe short string |
TextField | Long text |
BooleanField | TRUE/FALSE |
DateTimeField | Timestamp; auto_now updates on save |
Meta.ordering | Default sort for QuerySet |
__str__ | Human-readable in admin and shell |
Field Types Reference (Common)
| Django field | Use case |
|---|---|
IntegerField, BigIntegerField | Numbers |
DecimalField(max_digits, decimal_places) | Money |
EmailField, URLField | Validated strings |
DateField, DateTimeField | Dates/times |
ForeignKey | Many-to-one — chapter 10 |
ManyToManyField | Many-to-many |
JSONField | JSON column (PostgreSQL/MySQL 5.7+) |
Options: null=True (DB NULL), blank=True (form validation), default=, choices=.
Database Configuration
SQLite (development default)
Already in settings.py:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": BASE_DIR / "db.sqlite3",
}
}File db.sqlite3 appears after first migrate.
MySQL (production preview)
Install driver:
pip install mysqlclientOr on Windows if build fails:
pip install pymysqlconfig/__init__.py (PyMySQL only):
import pymysql
pymysql.install_as_MySQLdb()settings/production.py (or env-driven DATABASES):
Align MySQL server charset with utf8mb4 — MySQL installation.
PostgreSQL: django.db.backends.postgresql + psycopg[binary] — same ORM API.
Migrations Workflow (First Run)
python manage.py makemigrations blog
python manage.py migrateExpected:
Migrations for 'blog':
blog/migrations/0001_initial.py
+ Create model Postmigrate applies all apps (including auth, admin) and creates db.sqlite3.
Inspect SQL (optional):
python manage.py sqlmigrate blog 0001Full migration workflow (alter fields, data migrations, team rules): Database Migrations. That chapter adds a summary field to Post as a worked example.
CRUD via Shell
python manage.py shellQuerySet Basics
Post.objects.all() # SELECT *
Post.objects.filter(published=True) # WHERE
Post.objects.exclude(published=False)
Post.objects.order_by("title")
Post.objects.filter(title__icontains="django") # LIKE
Post.objects.count()
Post.objects.first()
Post.objects.get(slug="hello-orm") # raises DoesNotExist if missingChaining:
Post.objects.filter(published=True).order_by("-created_at")[:5]Lazy evaluation: the query runs when you iterate, slice, or call list().
get_or_create and update_or_create
post, created = Post.objects.get_or_create(
slug="welcome",
defaults={
"title": "Welcome",
"body": "Default body",
"published": True,
},
)created is True only on insert—useful for seed data and idempotent scripts.
Use Model in Views
blog/views.py:
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(published=True)
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk, published=True)
return render(request, "blog/post_detail.html", {"post": post})blog/templates/blog/post_detail.html:
{% extends "blog/base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<time>{{ post.created_at|date:"Y-m-d" }}</time>
<div>{{ post.body|linebreaks }}</div>
</article>
{% endblock %}linebreaks filter converts newlines to <p> tags.
Register in Admin (Preview)
blog/admin.py:
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "published", "created_at"]
list_filter = ["published"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ("title",)}Create superuser and log in — chapter 12.
Model Design Tips
- One model class per concept (
Post,Comment,Tag) - Use
slugfor URLs; keeppkfor admin and APIs - Put
orderinginMeta, not scattered in views - Avoid
null=TrueonCharField—preferblank=Truewithdefault=""
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Edit model, skip makemigrations | Schema drift | Run makemigrations then migrate |
get() with no row | DoesNotExist | filter().first() or get_object_or_404 |
mysqlclient build fail on Windows | pip error | Use pymysql shim or WSL |
Duplicate slug | IntegrityError | unique=True or handle in forms |
Post-Chapter Checklist
-
Postmodel defined withMetaand__str__ -
makemigrations/migratesucceeded - CRUD works in
shell -
post_list/post_detailuse realQuerySet - You know SQLite vs MySQL
ENGINEstrings
FAQ
CharField max_length required?
Yes for CharField. Use TextField for unbounded text.
Auto primary key?
Default id BigAutoField — DEFAULT_AUTO_FIELD in settings.
Raw SQL?
Post.objects.raw("SELECT ...") or connection.cursor() — escape user input; prefer ORM.
Multiple databases?
using="replica" on QuerySet — advanced routing in settings.
Timezone-aware datetimes?
Keep USE_TZ = True; store UTC, display with template date filter in local TZ.