Every Django application revolves around its data models. Whether you are building a blog, an e-commerce platform, or an API, the model layer defines how data is structured, stored, and retrieved. This lesson covers everything from defining your first model to running migrations and querying related data with Django's powerful ORM.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Define Django model classes using common field types
  • Create and apply database migrations to sync models with the database
  • Use the Django ORM to create, read, update, and delete records
  • Model relationships between data entities using ForeignKey and ManyToManyField
  • Query related data efficiently with select_related and prefetch_related
  • Design clean database schemas following Django best practices
  • Register models in the Django admin panel for data management

2. Why This Matters

Imagine building a blog where you store posts in a text file and users in another file. As soon as you need to answer questions like "which posts did this user write?" or "show me all comments on posts tagged with Python," a flat file approach collapses under its own complexity. Django models solve this by giving you a declarative, Pythonic way to define your database schema once. The ORM then handles all the SQL generation, relationship management, and data validation. In production DevOps stacks, the model layer is what connects your application to PostgreSQL, MySQL, or any supported database — and getting it right from day one saves weeks of painful migrations later.

3. Core Concepts

Model — A Python class that subclasses django.db.models.Model. Each model maps to a single database table. Each attribute of the model represents a database field.

Field Types — Django ships with dozens of field types including CharField (short strings, requires max_length), TextField (long text), IntegerField, BooleanField, DateTimeField, EmailField, URLField, and FileField. Each field type provides built-in validation and determines the column type in the database.

Migrations — Django generates migration files from changes to your model classes. Each migration is a Python script that describes how to alter the database schema. Running python manage.py migrate applies pending migrations to your database.

ORM (Object-Relational Mapper) — The Django ORM lets you query the database using Python methods instead of raw SQL. Example: Post.objects.filter(published=True).order_by('-created_at') generates a SELECT ... WHERE published=TRUE ORDER BY created_at DESC query behind the scenes.

Relationships — Three core types: ForeignKey (many-to-one, e.g., many posts belong to one author), ManyToManyField (many-to-many, e.g., a post can have many tags and a tag can appear on many posts), and OneToOneField (one-to-one, e.g., one user profile per user).

4. Hands-On Practice: Building a Blog Model

Let us build the models for a simple blog application step by step. Open your Django project and navigate to the models.py file inside your app directory.

4.1. Define the Author Model

from django.db import models
from django.utils import timezone

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    bio = models.TextField(blank=True)
    website = models.URLField(blank=True)
    joined_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.name
Author model with common field types

Notice unique=True on email — this creates a database-level unique constraint. blank=True makes the field optional in forms and the admin panel. default=timezone.now auto-sets the join date when a new author is created.

4.2. Define the Tag Model

class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True)

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name
Tag model with a slug field for URL-friendly identifiers

The SlugField automatically converts a name like "Python Tips" into "python-tips" for use in URLs. The inner Meta class tells Django to order tag queries by name by default.

4.3. Define the Post Model with Relationships

class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    content = models.TextField()
    excerpt = models.TextField(max_length=300, blank=True)
    author = models.ForeignKey(
        Author,
        on_delete=models.CASCADE,
        related_name='posts'
    )
    tags = models.ManyToManyField(Tag, blank=True,
        related_name='posts')
    published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title
Post model with ForeignKey and ManyToManyField relationships

Key relationship details:

  • ForeignKey(Author, on_delete=models.CASCADE) — when an author is deleted, all their posts are also deleted. Other options include PROTECT (block deletion), SET_NULL (set author to null), and SET_DEFAULT.
  • related_name='posts' — lets you access author.posts.all() from the Author side of the relationship.
  • auto_now_add=True — sets the timestamp only on creation. auto_now=True — updates the timestamp on every save.

4.4. Create and Apply Migrations

# Generate migration files from your model changes
python manage.py makemigrations

# Output:
# Migrations for 'blog':
#   blog/migrations/0001_initial.py

# Apply all pending migrations to the database
python manage.py migrate

# Output:
# Operations to perform:
#   Apply all migrations: blog
# Running migrations:
#   Applying blog.0001_initial... OK
Creating and applying migrations

Always run makemigrations after changing your models, then migrate to apply. Commit both the model changes and the generated migration files to version control — they are essential for reproducing the database schema in production.

4.5. Register Models in the Admin Panel

# admin.py
from django.contrib import admin
from .models import Author, Tag, Post

@admin.register(Author)
class AuthorAdmin(admin.ModelAdmin):
    list_display = ['name', 'email', 'joined_date']

@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    prepopulated_fields = {'slug': ['name']}

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'author', 'published', 'created_at']
    list_filter = ['published', 'tags', 'created_at']
    search_fields = ['title', 'content']
    prepopulated_fields = {'slug': ['title']}
Registering models in the Django admin

4.6. Query Data with the Django ORM

# --- CREATE ---
author = Author.objects.create(
    name='Katherine Johnson',
    email='katherine@example.com'
)

# --- READ ---
all_posts = Post.objects.all()
published = Post.objects.filter(published=True)
recent = Post.objects.filter(
    created_at__gte='2025-01-01'
)[:5]

# --- UPDATE ---
post = Post.objects.get(id=1)
post.title = 'Updated Title'
post.save()

# --- DELETE ---
post.delete()

# --- RELATED DATA (reverse FK) ---
authors_posts = author.posts.all()

# --- MANY-TO-MANY ---
tag = Tag.objects.get(name='Python')
post.tags.add(tag)
posts_with_tag = tag.posts.all()
CRUD operations with the Django ORM

The double-underscore syntax (created_at__gte) powers field lookups. Common lookups include __exact, __contains, __startswith, __in, __gt (greater than), __lt (less than), and __range (between two values).

4.7. Optimize Queries for Relationships

# N+1 query problem: 1 query for posts + N queries for authors
posts = Post.objects.all()
for p in posts:
    print(p.author.name)  # Hits DB once per post!

# Solution: select_related for ForeignKey/OneToOne
posts = Post.objects.select_related('author').all()
for p in posts:
    print(p.author.name)  # Zero extra queries

# Solution: prefetch_related for ManyToManyField
posts = Post.objects.prefetch_related('tags').all()
for p in posts:
    print([t.name for t in p.tags.all()])  # 2 queries total
Fixing the N+1 query problem with select_related and prefetch_related

5. Common Errors & Solutions

Error 1: "No migrations to apply" after creating a model

Cause: You may have forgotten to run makemigrations after adding the model class, or the app is not listed in INSTALLED_APPS.

Solution: Add your app to INSTALLED_APPS in settings.py, then run python manage.py makemigrations your_app_name followed by migrate.

Error 2: "Field X has an invalid choice"

Cause: You added a choices argument but provided a value not in the defined choices tuple.

Solution: Use a constant for each choice and always reference the constant, not a hardcoded string. Example: STATUS_PUBLISHED = 'published'.

Error 3: "django.db.utils.OperationalError: no such column"

Cause: You added a field to a model but forgot to run migrate, or you re-created the database without reapplying migrations.

Solution: Run python manage.py migrate. If the migration already exists, reset with python manage.py migrate app_name zero then migrate app_name again.

Error 4: "Cannot resolve keyword X into field"

Cause: You used a field name in a filter() call that does not exist on the model, or you misspelled a lookup like __gth instead of __gte.

Solution: Check the exact field name in your model class and the available lookups in the Django documentation.

Error 5: Migration conflict — "Conflicting migrations detected"

Cause: Two developers created migrations from the same parent migration, resulting in a merge conflict in the migration history.

Solution: Run python manage.py makemigrations --merge to generate a merge migration that reconciles both branches.

6. Summary Checklist

  • Each model class inherits from models.Model and maps to a database table
  • Choose field types carefully — they affect validation, storage, and indexing
  • Run makemigrations after every model change, then migrate to apply
  • Use ForeignKey for many-to-one, ManyToManyField for many-to-many relationships
  • Always specify on_delete behavior for ForeignKey fields
  • Register models in admin.py for data management through the admin panel
  • Use select_related for ForeignKeys and prefetch_related for ManyToManyFields to avoid N+1 queries
  • Commit migration files to version control alongside model changes
  • Use __str__ to give each model a human-readable representation
  • Use the Meta class for default ordering, verbose names, and constraints

7. Practice Exercise

Build a Comment model for the blog application. Your model should:

  • Be linked to a Post via ForeignKey
  • Store the commenter's name, email, and website (URL) — all optional except name
  • Store the comment body as TextField
  • Track created_at (auto-set on creation)
  • Have a approved boolean field defaulting to False

Bonus challenge: Add a parent ForeignKey to the Comment model itself, so comments can be nested as replies. Use related_name='replies' and make the field optional (blank=True, null=True).

Stretch goal: Create a custom model manager called PublishedManager that only returns approved comments, and use it as the default manager: objects = PublishedManager().

8. Next Steps

Now that you understand Django models, the next lesson covers Django REST Framework serializers and viewsets. You will learn how to expose your models through a RESTful API with full CRUD support, authentication, and permission controls — turning your blog models into a fully functional API backend ready for a React or Vue frontend.

In the meantime, practice by adding the Comment model above, creating and applying its migration, and testing the relationship by creating a Post with several comments via the Django shell: python manage.py shell.

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen July 24, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment