Skip to content

Django Models#

Complete guide to defining database models with all field types and relationships.

🎯 Basic Model#

# myapp/models.py
from django.db import models
from django.utils import timezone

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

    def __str__(self):
        return self.title

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Post'
        verbose_name_plural = 'Posts'

📋 All Field Types#

String Fields#

# CharField - Short strings
title = models.CharField(max_length=200)
name = models.CharField(max_length=100, blank=True)  # Optional
slug = models.CharField(max_length=200, unique=True)

# TextField - Long text
content = models.TextField()
description = models.TextField(blank=True, null=True)

# EmailField - Email validation
email = models.EmailField(max_length=254)
email = models.EmailField(unique=True)

# URLField - URL validation
website = models.URLField(max_length=200)
website = models.URLField(blank=True)

# SlugField - URL-friendly string
slug = models.SlugField(max_length=200, unique=True)
slug = models.SlugField(allow_unicode=True)  # Allow non-ASCII

Numeric Fields#

# IntegerField
age = models.IntegerField()
quantity = models.IntegerField(default=0)
price = models.IntegerField(blank=True, null=True)

# PositiveIntegerField
views = models.PositiveIntegerField(default=0)
likes = models.PositiveIntegerField()

# SmallIntegerField
status = models.SmallIntegerField(default=1)

# BigIntegerField
user_id = models.BigIntegerField()

# DecimalField - Precise decimal numbers
price = models.DecimalField(max_digits=10, decimal_places=2)
# max_digits = total digits, decimal_places = digits after decimal

# FloatField - Floating point
rating = models.FloatField()
rating = models.FloatField(default=0.0)

Date/Time Fields#

# DateField
birth_date = models.DateField()
published_date = models.DateField(auto_now_add=True)
updated_date = models.DateField(auto_now=True)

# DateTimeField
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)

# TimeField
start_time = models.TimeField()
end_time = models.TimeField()

# DurationField
duration = models.DurationField()

Boolean Fields#

# BooleanField
is_active = models.BooleanField(default=True)
is_published = models.BooleanField(default=False)

# NullBooleanField (deprecated, use BooleanField with null=True)
is_featured = models.BooleanField(null=True, blank=True)

File Fields#

# FileField - Any file
document = models.FileField(upload_to='documents/')
document = models.FileField(upload_to='documents/%Y/%m/%d/')  # Date-based folders

# ImageField - Images (requires Pillow)
photo = models.ImageField(upload_to='photos/')
photo = models.ImageField(upload_to='photos/', blank=True, null=True)
photo = models.ImageField(upload_to='photos/', height_field='height', width_field='width')

# FilePathField - File path from directory
config_file = models.FilePathField(path='/etc/config/')

Choice Fields#

# Choices tuple
STATUS_CHOICES = [
    ('draft', 'Draft'),
    ('published', 'Published'),
    ('archived', 'Archived'),
]

status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')

# Integer choices
PRIORITY_CHOICES = [
    (1, 'Low'),
    (2, 'Medium'),
    (3, 'High'),
]

priority = models.IntegerField(choices=PRIORITY_CHOICES, default=2)

Other Fields#

# UUIDField
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

# JSONField (PostgreSQL, MySQL 5.7+)
metadata = models.JSONField(default=dict)
metadata = models.JSONField(default=list)

# BinaryField
data = models.BinaryField()

# GenericIPAddressField
ip_address = models.GenericIPAddressField()
ip_address = models.GenericIPAddressField(protocol='IPv4')

🔗 Relationships#

ForeignKey (Many-to-One)#

from django.contrib.auth.models import User

class Post(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)

    def __str__(self):
        return self.title

class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

# Access related objects
post = Post.objects.get(id=1)
author = post.author  # Get author
posts = author.post_set.all()  # Get all posts by author

# Reverse relationship with related_name
class Post(models.Model):
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    # Now use: author.posts.all() instead of author.post_set.all()

ManyToManyField#

class Post(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField('Tag', blank=True)

    def __str__(self):
        return self.title

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

    def __str__(self):
        return self.name

# Access related objects
post = Post.objects.get(id=1)
tags = post.tags.all()  # Get all tags
posts = tag.post_set.all()  # Get all posts with tag

# Add/remove relationships
post.tags.add(tag1, tag2)
post.tags.remove(tag1)
post.tags.clear()
post.tags.set([tag1, tag2])  # Replace all

# Through model (extra fields on relationship)
class PostTag(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)

class Post(models.Model):
    tags = models.ManyToManyField('Tag', through='PostTag')

OneToOneField#

from django.contrib.auth.models import User

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True)
    phone = models.CharField(max_length=20, blank=True)

    def __str__(self):
        return f"{self.user.username}'s Profile"

# Access
user = User.objects.get(id=1)
profile = user.profile  # Get profile
user = profile.user  # Get user

🗑️ on_delete Options#

# CASCADE - Delete related objects
author = models.ForeignKey(User, on_delete=models.CASCADE)
# If user deleted, all posts deleted

# PROTECT - Prevent deletion
category = models.ForeignKey(Category, on_delete=models.PROTECT)
# Cannot delete category if posts exist

# SET_NULL - Set to NULL (requires null=True)
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
# If category deleted, set post.category to NULL

# SET_DEFAULT - Set to default value
category = models.ForeignKey(Category, on_delete=models.SET_DEFAULT, default=1)
# If category deleted, set to default category

# DO_NOTHING - Do nothing (database constraint)
category = models.ForeignKey(Category, on_delete=models.DO_NOTHING)
# Manual handling required

# SET() - Set to callable
def get_default_category():
    return Category.objects.get_or_create(name='Uncategorized')[0]

category = models.ForeignKey(Category, on_delete=models.SET(get_default_category))

🏷️ Model Meta Options#

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

    class Meta:
        # Ordering
        ordering = ['-created_at']  # Descending
        ordering = ['created_at']  # Ascending
        ordering = ['-created_at', 'title']  # Multiple fields

        # Database table name
        db_table = 'blog_posts'

        # Verbose names
        verbose_name = 'Post'
        verbose_name_plural = 'Posts'

        # Indexes
        indexes = [
            models.Index(fields=['created_at']),
            models.Index(fields=['title', 'created_at']),
        ]

        # Unique constraints
        unique_together = [['title', 'author']]
        # Or in Django 2.2+
        constraints = [
            models.UniqueConstraint(fields=['title', 'author'], name='unique_title_author'),
        ]

        # Permissions
        permissions = [
            ('can_publish', 'Can publish posts'),
        ]

        # Default manager
        default_manager_name = 'objects'

🎯 Model Methods#

str Method#

class Post(models.Model):
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title
    # Shows in admin, shell, etc.

Custom Methods#

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    created_at = models.DateTimeField(auto_now_add=True)
    views = models.PositiveIntegerField(default=0)

    def get_absolute_url(self):
        from django.urls import reverse
        return reverse('myapp:post_detail', kwargs={'pk': self.pk})

    def get_excerpt(self, length=100):
        return self.content[:length] + '...' if len(self.content) > length else self.content

    def increment_views(self):
        self.views += 1
        self.save(update_fields=['views'])

    @property
    def is_recent(self):
        from django.utils import timezone
        from datetime import timedelta
        return self.created_at >= timezone.now() - timedelta(days=7)

Model Managers#

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_published=True)

class Post(models.Model):
    title = models.CharField(max_length=200)
    is_published = models.BooleanField(default=False)

    objects = models.Manager()  # Default manager
    published = PublishedManager()  # Custom manager

    def __str__(self):
        return self.title

# Usage
Post.objects.all()  # All posts
Post.published.all()  # Only published posts

📝 Field Options#

# null - Database level NULL
field = models.CharField(max_length=100, null=True)

# blank - Form validation (allows empty in forms)
field = models.CharField(max_length=100, blank=True)

# default - Default value
field = models.CharField(max_length=100, default='default')
field = models.IntegerField(default=0)
field = models.BooleanField(default=True)

# choices - Limit choices
STATUS_CHOICES = [('draft', 'Draft'), ('published', 'Published')]
status = models.CharField(max_length=20, choices=STATUS_CHOICES)

# unique - Must be unique
email = models.EmailField(unique=True)

# db_index - Create database index
title = models.CharField(max_length=200, db_index=True)

# editable - Can be edited in admin
created_at = models.DateTimeField(auto_now_add=True, editable=False)

# help_text - Help text in admin
email = models.EmailField(help_text='Enter a valid email address')

# verbose_name - Human-readable name
first_name = models.CharField(max_length=100, verbose_name='First Name')

✅ Complete Example#

from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse

class Category(models.Model):
    name = models.CharField(max_length=100, unique=True)
    slug = models.SlugField(unique=True)
    description = models.TextField(blank=True)

    class Meta:
        verbose_name_plural = 'Categories'
        ordering = ['name']

    def __str__(self):
        return self.name

    def get_absolute_url(self):
        return reverse('myapp:category_detail', kwargs={'slug': self.slug})

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

    def __str__(self):
        return self.name

class Post(models.Model):
    STATUS_CHOICES = [
        ('draft', 'Draft'),
        ('published', 'Published'),
        ('archived', 'Archived'),
    ]

    title = models.CharField(max_length=200)
    slug = models.SlugField(unique=True)
    content = models.TextField()
    excerpt = models.TextField(max_length=300, blank=True)

    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name='posts')
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True)
    tags = models.ManyToManyField(Tag, blank=True)

    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='draft')
    is_featured = models.BooleanField(default=False)

    views = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['status', 'is_featured']),
        ]

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse('myapp:post_detail', kwargs={'slug': self.slug})

    def increment_views(self):
        self.views += 1
        self.save(update_fields=['views'])

✅ Next Steps#

  • Create migrations: python manage.py makemigrations
  • Apply migrations: python manage.py migrate
  • Learn ORM Queries to query these models
  • Learn Admin Customization to manage in admin

Pro Tip: Always define __str__ method for your models - it makes debugging and admin much easier!