DEV Community

Cover image for Day 67 of 100 Days Of Code — Django Models and ORM
M Saad Ahmad
M Saad Ahmad

Posted on

Day 67 of 100 Days Of Code — Django Models and ORM

Yesterday, I learned URLs, views, and templates. Everything was hardcoded, though. Today, for day 67, I connected Django to an actual database. Models, migrations, the ORM, and the admin panel. By the end, I had a page pulling real data from a real database and displaying it in the browser.


What is a Model?

A model is a Python class that represents a database table. Each attribute is a column. Django handles all the SQL; you never write CREATE TABLE or SELECT * directly.

from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    is_published = models.BooleanField(default=False)

    def __str__(self):
        return self.title
Enter fullscreen mode Exit fullscreen mode

This class tells Django to create a post table with four columns. The __str__ method controls how a Post object is displayed in the admin panel, in the shell, everywhere.


Common Field Types

Field Use
CharField Short text, requires max_length
TextField Long text, no max length
IntegerField Whole numbers
FloatField Decimal numbers
BooleanField True or False
DateTimeField Date and time
DateField Date only
EmailField Validates email format
ForeignKey Many-to-one relationship
ManyToManyField Many-to-many relationship

Field Options

Most fields accept these common options:

class Post(models.Model):
    title = models.CharField(max_length=200, unique=True)
    content = models.TextField(blank=True)
    views = models.IntegerField(default=0)
    slug = models.SlugField(null=True, blank=True)
Enter fullscreen mode Exit fullscreen mode
  • null=True — allows NULL in the database
  • blank=True — allows empty value in forms
  • default — sets a default value
  • unique=True — enforces unique values across rows

null and blank are different. null is database-level, blank is validation-level.


The Meta Class

The inner Meta class lets you configure model-level behavior:

class Post(models.Model):
    title = models.CharField(max_length=200)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['-created_at']  # newest first
        verbose_name = 'Post'
        verbose_name_plural = 'Posts'
Enter fullscreen mode Exit fullscreen mode

ordering sets the default order for all queries on this model. The - prefix means descending.


Migrations

After defining your model, you don't touch the database directly. You run two commands:

python manage.py makemigrations
python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

makemigrations looks at your models, detects what changed, and generates a migration file in migrations/. This file is a Python description of the database change.

migrate takes all pending migration files and applies them to the actual database.

Check migration status anytime:

python manage.py showmigrations
Enter fullscreen mode Exit fullscreen mode

Every time you change a model, add a field, remove a field, change a field type. You run both commands again. Never edit the database manually.


Django ORM — CRUD

The ORM lets you interact with the database using Python instead of SQL. Every model gets a built-in objects manager.

Create

# option 1
post = Post(title="My First Post", content="Hello world")
post.save()

# option 2 — cleaner
post = Post.objects.create(title="My First Post", content="Hello world")
Enter fullscreen mode Exit fullscreen mode

Read

# get all posts
posts = Post.objects.all()

# get one post by id — raises exception if not found
post = Post.objects.get(id=1)

# filter — returns a queryset
published = Post.objects.filter(is_published=True)

# exclude
drafts = Post.objects.exclude(is_published=True)

# chaining
recent_published = Post.objects.filter(is_published=True).order_by('-created_at')

# first and last
latest = Post.objects.order_by('-created_at').first()
Enter fullscreen mode Exit fullscreen mode

Update

# update one object
post = Post.objects.get(id=1)
post.title = "Updated Title"
post.save()

# bulk update
Post.objects.filter(is_published=False).update(is_published=True)
Enter fullscreen mode Exit fullscreen mode

Delete

# delete one
post = Post.objects.get(id=1)
post.delete()

# bulk delete
Post.objects.filter(is_published=False).delete()
Enter fullscreen mode Exit fullscreen mode

QuerySets Are Lazy

When we write:

posts = Post.objects.filter(is_published=True)
Enter fullscreen mode Exit fullscreen mode

Django does not hit the database yet. It builds a QuerySet: a description of the query. The database is only hit when you actually use the data, iterating over it, slicing it, calling len() on it.

posts = Post.objects.filter(is_published=True)  # no DB hit yet
for post in posts:  # DB hit happens here
    print(post.title)
Enter fullscreen mode Exit fullscreen mode

This matters for performance. You can chain filters and conditions before Django ever touches the database.


Django Admin

Django ships with a built-in admin panel. You register your models there and get a full CRUD interface for free.

First, create a superuser:

python manage.py createsuperuser
Enter fullscreen mode Exit fullscreen mode

Then register your model in core/admin.py:

from django.contrib import admin
from .models import Post

admin.site.register(Post)
Enter fullscreen mode Exit fullscreen mode

Visit http://127.0.0.1:8000/admin/, log in, and you can create, edit, and delete posts directly from the browser. No code needed.

Customizing the Admin

@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ['title', 'is_published', 'created_at']
    list_filter = ['is_published']
    search_fields = ['title', 'content']
Enter fullscreen mode Exit fullscreen mode

list_display controls which columns show in the list view. list_filter adds a sidebar filter. search_fields adds a search bar.


Connecting Models to Views and Templates

Now the full picture: fetching real data in a view and displaying it in a template.

# core/views.py
from django.shortcuts import render, get_object_or_404
from .models import Post

def home(request):
    posts = Post.objects.filter(is_published=True).order_by('-created_at')
    return render(request, 'core/home.html', {'posts': posts})

def post_detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'core/post_detail.html', {'post': post})
Enter fullscreen mode Exit fullscreen mode

get_object_or_404 is a Django shortcut. It tries to get the object and automatically returns a 404 response if it doesn't exist. Use this instead of get() in views.

# core/urls.py
from django.urls import path
from . import views

urlpatterns = [
    path('', views.home, name='home'),
    path('post/<int:pk>/', views.post_detail, name='post_detail'),
]
Enter fullscreen mode Exit fullscreen mode

<int:pk> is a URL parameter — Django captures whatever integer is in that position and passes it to the view as pk.

<!-- core/templates/core/home.html -->
{% extends 'core/base.html' %}

{% block content %}
    <h1>Posts</h1>
    {% for post in posts %}
        <div>
            <h2>{{ post.title }}</h2>
            <p>{{ post.created_at }}</p>
            <a href="{% url 'post_detail' post.pk %}">Read more</a>
        </div>
    {% empty %}
        <p>No posts yet.</p>
    {% endfor %}
{% endblock %}
Enter fullscreen mode Exit fullscreen mode
<!-- core/templates/core/post_detail.html -->
{% extends 'core/base.html' %}

{% block content %}
    <h1>{{ post.title }}</h1>
    <p>{{ post.created_at }}</p>
    <p>{{ post.content }}</p>
{% endblock %}
Enter fullscreen mode Exit fullscreen mode

Two new things here:

  • {% url 'post_detail' post.pk %} — generates a URL by name instead of hardcoding it
  • {% empty %} — inside a for loop, renders if the list is empty

The Full Picture So Far

Browser requests /
        ↓
urls.py → views.home
        ↓
views.home → Post.objects.filter() → hits database
        ↓
QuerySet passed to render() as context
        ↓
home.html loops through posts, renders HTML
        ↓
Response sent back to browser
Enter fullscreen mode Exit fullscreen mode

The only thing that changed from yesterday is step 3: instead of hardcoded data, the view now fetches real data from the database.


Wrapping Up

Today was the missing piece from yesterday. The page is now alive, real data, real database, real queries. Models define the structure, migrations apply it, the ORM handles all reads and writes, and the admin panel gives you a free interface to manage everything.

Tomorrow: Django Forms: letting users actually submit data.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)