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!