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
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
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
Key relationship details:
ForeignKey(Author, on_delete=models.CASCADE)— when an author is deleted, all their posts are also deleted. Other options includePROTECT(block deletion),SET_NULL(set author to null), andSET_DEFAULT.related_name='posts'— lets you accessauthor.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
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']}
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()
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
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.Modeland maps to a database table - Choose field types carefully — they affect validation, storage, and indexing
- Run
makemigrationsafter every model change, thenmigrateto apply - Use
ForeignKeyfor many-to-one,ManyToManyFieldfor many-to-many relationships - Always specify
on_deletebehavior for ForeignKey fields - Register models in
admin.pyfor data management through the admin panel - Use
select_relatedfor ForeignKeys andprefetch_relatedfor 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
Metaclass 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
PostviaForeignKey - 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
approvedboolean field defaulting toFalse
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.
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment