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

Define the Post Model

blog/models.py:

Code explanation:

FieldMaps to
CharFieldVARCHAR with max_length
SlugFieldURL-safe short string
TextFieldLong text
BooleanFieldTRUE/FALSE
DateTimeFieldTimestamp; auto_now updates on save
Meta.orderingDefault sort for QuerySet
__str__Human-readable in admin and shell

Field Types Reference (Common)

Django fieldUse case
IntegerField, BigIntegerFieldNumbers
DecimalField(max_digits, decimal_places)Money
EmailField, URLFieldValidated strings
DateField, DateTimeFieldDates/times
ForeignKeyMany-to-one — chapter 10
ManyToManyFieldMany-to-many
JSONFieldJSON 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:

python
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.sqlite3",
        "NAME": BASE_DIR / "db.sqlite3",
    }
}

File db.sqlite3 appears after first migrate.

MySQL (production preview)

Install driver:

bash
pip install mysqlclient

Or on Windows if build fails:

bash
pip install pymysql

config/__init__.py (PyMySQL only):

python
import pymysql
 
pymysql.install_as_MySQLdb()

settings/production.py (or env-driven DATABASES):

Align MySQL server charset with utf8mb4MySQL installation.

PostgreSQL: django.db.backends.postgresql + psycopg[binary] — same ORM API.

Migrations Workflow (First Run)

bash
python manage.py makemigrations blog
python manage.py migrate

Expected:

text
Migrations for 'blog':
  blog/migrations/0001_initial.py
    + Create model Post

migrate applies all apps (including auth, admin) and creates db.sqlite3.

Inspect SQL (optional):

bash
python manage.py sqlmigrate blog 0001

Full 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

bash
python manage.py shell

QuerySet Basics

python
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 missing

Chaining:

python
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

python
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:

python
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:

django
{% 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:

python
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 slug for URLs; keep pk for admin and APIs
  • Put ordering in Meta, not scattered in views
  • Avoid null=True on CharField—prefer blank=True with default=""

Common Mistakes

MistakeSymptomFix
Edit model, skip makemigrationsSchema driftRun makemigrations then migrate
get() with no rowDoesNotExistfilter().first() or get_object_or_404
mysqlclient build fail on Windowspip errorUse pymysql shim or WSL
Duplicate slugIntegrityErrorunique=True or handle in forms

Post-Chapter Checklist

  • Post model defined with Meta and __str__
  • makemigrations / migrate succeeded
  • CRUD works in shell
  • post_list / post_detail use real QuerySet
  • You know SQLite vs MySQL ENGINE strings

FAQ

CharField max_length required?

Yes for CharField. Use TextField for unbounded text.

Auto primary key?

Default id BigAutoFieldDEFAULT_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.