Django's Object-Relational Mapping (ORM) system provides a powerful abstraction layer between your Python code and the database. This chapter covers everything from basic model definition to advanced database operations, query optimization, and working with multiple databases.
Django models are Python classes that define the structure and behavior of your application's data. Each model represents a database table, and each attribute of the model represents a database field. The Django ORM handles the translation between Python objects and database records automatically.
Database Abstraction: Write database-agnostic code that works across different database engines Automatic Schema Generation: Django creates and manages database tables based on your model definitions Query Interface: Intuitive Python API for database operations without writing SQL Data Validation: Built-in field validation and custom validation methods Relationship Management: Elegant handling of foreign keys and many-to-many relationships Migration System: Automatic schema evolution and version control for database changes
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils import timezone
class Category(models.Model):
name = models.CharField(max_length=100, unique=True)
slug = models.SlugField(max_length=100, unique=True)
description = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
verbose_name_plural = "Categories"
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('category_detail', kwargs={'slug': self.slug})
class Post(models.Model):
STATUS_CHOICES = [
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived'),
]
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=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)
content = models.TextField()
excerpt = models.TextField(max_length=300, blank=True)
status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
featured_image = models.ImageField(upload_to='posts/', blank=True, null=True)
tags = models.ManyToManyField('Tag', blank=True)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
published_at = models.DateTimeField(null=True, blank=True)
# SEO fields
meta_title = models.CharField(max_length=60, blank=True)
meta_description = models.CharField(max_length=160, blank=True)
# Statistics
view_count = models.PositiveIntegerField(default=0)
like_count = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['status', 'published_at']),
models.Index(fields=['author', 'status']),
models.Index(fields=['category', 'status']),
]
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post_detail', kwargs={'slug': self.slug})
def save(self, *args, **kwargs):
# Auto-set published_at when status changes to published
if self.status == 'published' and not self.published_at:
self.published_at = timezone.now()
# Generate excerpt if not provided
if not self.excerpt and self.content:
self.excerpt = self.content[:297] + '...' if len(self.content) > 300 else self.content
super().save(*args, **kwargs)
@property
def is_published(self):
return self.status == 'published' and self.published_at <= timezone.now()
def get_reading_time(self):
"""Calculate estimated reading time in minutes"""
word_count = len(self.content.split())
return max(1, word_count // 200) # Assume 200 words per minute
class Tag(models.Model):
name = models.CharField(max_length=50, unique=True)
slug = models.SlugField(max_length=50, unique=True)
color = models.CharField(max_length=7, default='#007bff') # Hex color
class Meta:
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('tag_detail', kwargs={'slug': self.slug})
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
author = models.ForeignKey(User, on_delete=models.CASCADE)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='replies')
content = models.TextField()
is_approved = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['created_at']
indexes = [
models.Index(fields=['post', 'is_approved']),
models.Index(fields=['author', 'created_at']),
]
def __str__(self):
return f'Comment by {self.author.username} on {self.post.title}'
@property
def is_reply(self):
return self.parent is not None
# models.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
from django.contrib.auth.models import User
import uuid
class Product(models.Model):
# Primary key (auto-generated if not specified)
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
# Text fields
name = models.CharField(max_length=200)
description = models.TextField()
short_description = models.CharField(max_length=500, blank=True)
slug = models.SlugField(max_length=200, unique=True)
# Numeric fields
price = models.DecimalField(max_digits=10, decimal_places=2)
cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
weight = models.FloatField(help_text="Weight in kilograms")
quantity_in_stock = models.PositiveIntegerField(default=0)
minimum_stock_level = models.PositiveSmallIntegerField(default=5)
# Boolean fields
is_active = models.BooleanField(default=True)
is_featured = models.BooleanField(default=False)
requires_shipping = models.BooleanField(default=True)
# Date and time fields
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
available_from = models.DateField(null=True, blank=True)
available_until = models.DateField(null=True, blank=True)
# Choice fields
CONDITION_CHOICES = [
('new', 'New'),
('refurbished', 'Refurbished'),
('used', 'Used'),
]
condition = models.CharField(max_length=20, choices=CONDITION_CHOICES, default='new')
# File fields
main_image = models.ImageField(upload_to='products/images/', blank=True, null=True)
manual_pdf = models.FileField(upload_to='products/manuals/', blank=True, null=True)
# JSON field (PostgreSQL, MySQL 5.7+, SQLite 3.38+)
specifications = models.JSONField(default=dict, blank=True)
# Custom validators
sku = models.CharField(
max_length=20,
unique=True,
validators=[
RegexValidator(
regex=r'^[A-Z]{2}\d{4}[A-Z]{2}$',
message='SKU must be in format: XX0000XX'
)
]
)
rating = models.DecimalField(
max_digits=3,
decimal_places=2,
validators=[MinValueValidator(0.0), MaxValueValidator(5.0)],
null=True,
blank=True
)
class Meta:
ordering = ['-created_at']
indexes = [
models.Index(fields=['is_active', 'is_featured']),
models.Index(fields=['price']),
models.Index(fields=['created_at']),
]
constraints = [
models.CheckConstraint(
check=models.Q(price__gte=0),
name='positive_price'
),
models.CheckConstraint(
check=models.Q(quantity_in_stock__gte=0),
name='positive_stock'
),
]
def __str__(self):
return self.name
@property
def is_in_stock(self):
return self.quantity_in_stock > 0
@property
def is_low_stock(self):
return self.quantity_in_stock <= self.minimum_stock_level
def get_profit_margin(self):
if self.cost:
return ((self.price - self.cost) / self.price) * 100
return None
class ProductImage(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images')
image = models.ImageField(upload_to='products/gallery/')
alt_text = models.CharField(max_length=200)
is_primary = models.BooleanField(default=False)
order = models.PositiveSmallIntegerField(default=0)
class Meta:
ordering = ['order', 'id']
unique_together = [['product', 'order']]
def __str__(self):
return f'{self.product.name} - Image {self.order}'
class ProductReview(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='reviews')
user = models.ForeignKey(User, on_delete=models.CASCADE)
rating = models.PositiveSmallIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(5)]
)
title = models.CharField(max_length=200)
content = models.TextField()
is_verified_purchase = models.BooleanField(default=False)
helpful_votes = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
unique_together = [['product', 'user']]
ordering = ['-created_at']
def __str__(self):
return f'{self.rating}★ - {self.title}'
# models.py
from django.db import models
from django.contrib.auth.models import User
class Author(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
bio = models.TextField(blank=True)
website = models.URLField(blank=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return self.user.get_full_name() or self.user.username
class Publisher(models.Model):
name = models.CharField(max_length=200)
address = models.TextField()
city = models.CharField(max_length=100)
country = models.CharField(max_length=100)
website = models.URLField(blank=True)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
isbn = models.CharField(max_length=13, unique=True)
# Foreign Key relationships
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='books'
)
publisher = models.ForeignKey(
Publisher,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='books'
)
publication_date = models.DateField()
pages = models.PositiveIntegerField()
price = models.DecimalField(max_digits=8, decimal_places=2)
def __str__(self):
return self.title
# Usage examples:
# Get all books by an author
# author = Author.objects.get(user__username='john_doe')
# books = author.books.all()
# Get publisher of a book
# book = Book.objects.get(isbn='9781234567890')
# publisher = book.publisher
# Get all books from a publisher
# publisher = Publisher.objects.get(name='Penguin')
# books = publisher.books.all()
# models.py
from django.db import models
class Genre(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.TextField(blank=True)
def __str__(self):
return self.name
class Movie(models.Model):
title = models.CharField(max_length=200)
release_date = models.DateField()
duration = models.PositiveIntegerField(help_text="Duration in minutes")
# Simple many-to-many
genres = models.ManyToManyField(Genre, related_name='movies')
def __str__(self):
return self.title
class Actor(models.Model):
name = models.CharField(max_length=200)
birth_date = models.DateField()
def __str__(self):
return self.name
# Many-to-many with through model for additional fields
class MovieCast(models.Model):
ROLE_CHOICES = [
('lead', 'Lead Role'),
('supporting', 'Supporting Role'),
('cameo', 'Cameo'),
]
movie = models.ForeignKey(Movie, on_delete=models.CASCADE)
actor = models.ForeignKey(Actor, on_delete=models.CASCADE)
character_name = models.CharField(max_length=200)
role_type = models.CharField(max_length=20, choices=ROLE_CHOICES)
billing_order = models.PositiveSmallIntegerField()
class Meta:
unique_together = [['movie', 'actor']]
ordering = ['billing_order']
def __str__(self):
return f'{self.actor.name} as {self.character_name} in {self.movie.title}'
# Update Movie model to use through relationship
class Movie(models.Model):
title = models.CharField(max_length=200)
release_date = models.DateField()
duration = models.PositiveIntegerField(help_text="Duration in minutes")
# Simple many-to-many
genres = models.ManyToManyField(Genre, related_name='movies')
# Many-to-many with through model
actors = models.ManyToManyField(
Actor,
through='MovieCast',
related_name='movies'
)
def __str__(self):
return self.title
# Usage examples:
# Add genres to a movie
# movie = Movie.objects.get(title='The Matrix')
# action_genre = Genre.objects.get(name='Action')
# sci_fi_genre = Genre.objects.get(name='Science Fiction')
# movie.genres.add(action_genre, sci_fi_genre)
# Add actor with role information
# MovieCast.objects.create(
# movie=movie,
# actor=keanu_reeves,
# character_name='Neo',
# role_type='lead',
# billing_order=1
# )
# Get all movies in a genre
# action_movies = Genre.objects.get(name='Action').movies.all()
# Get cast for a movie
# cast = MovieCast.objects.filter(movie=movie).order_by('billing_order')
# models.py
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.exceptions import ValidationError
import hashlib
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
birth_date = models.DateField(null=True, blank=True)
phone = models.CharField(max_length=20, blank=True)
avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
bio = models.TextField(max_length=500, blank=True)
location = models.CharField(max_length=100, blank=True)
website = models.URLField(blank=True)
# Social media
twitter_handle = models.CharField(max_length=50, blank=True)
linkedin_url = models.URLField(blank=True)
# Preferences
email_notifications = models.BooleanField(default=True)
newsletter_subscription = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f'{self.user.username} Profile'
def clean(self):
"""Custom validation"""
if self.birth_date and self.birth_date > timezone.now().date():
raise ValidationError('Birth date cannot be in the future.')
if self.twitter_handle and not self.twitter_handle.startswith('@'):
self.twitter_handle = f'@{self.twitter_handle}'
def save(self, *args, **kwargs):
self.full_clean() # Run validation
super().save(*args, **kwargs)
@property
def age(self):
"""Calculate age from birth date"""
if not self.birth_date:
return None
today = timezone.now().date()
return today.year - self.birth_date.year - (
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
)
@property
def full_name(self):
"""Get user's full name"""
return self.user.get_full_name() or self.user.username
@property
def avatar_url(self):
"""Get avatar URL or default gravatar"""
if self.avatar:
return self.avatar.url
# Generate Gravatar URL
email_hash = hashlib.md5(self.user.email.lower().encode()).hexdigest()
return f'https://www.gravatar.com/avatar/{email_hash}?d=identicon&s=150'
def get_social_links(self):
"""Return dictionary of social media links"""
links = {}
if self.website:
links['website'] = self.website
if self.twitter_handle:
links['twitter'] = f'https://twitter.com/{self.twitter_handle.lstrip("@")}'
if self.linkedin_url:
links['linkedin'] = self.linkedin_url
return links
def is_profile_complete(self):
"""Check if profile has minimum required information"""
required_fields = [
self.user.first_name,
self.user.last_name,
self.user.email,
self.bio,
]
return all(field for field in required_fields)
class Meta:
verbose_name = 'User Profile'
verbose_name_plural = 'User Profiles'
class Order(models.Model):
STATUS_CHOICES = [
('pending', 'Pending'),
('processing', 'Processing'),
('shipped', 'Shipped'),
('delivered', 'Delivered'),
('cancelled', 'Cancelled'),
('refunded', 'Refunded'),
]
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders')
order_number = models.CharField(max_length=20, unique=True, editable=False)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
# Pricing
subtotal = models.DecimalField(max_digits=10, decimal_places=2)
tax_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
shipping_cost = models.DecimalField(max_digits=10, decimal_places=2, default=0)
discount_amount = models.DecimalField(max_digits=10, decimal_places=2, default=0)
# Addresses
billing_address = models.TextField()
shipping_address = models.TextField()
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
shipped_at = models.DateTimeField(null=True, blank=True)
delivered_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f'Order {self.order_number}'
def save(self, *args, **kwargs):
if not self.order_number:
self.order_number = self.generate_order_number()
super().save(*args, **kwargs)
def generate_order_number(self):
"""Generate unique order number"""
import random
import string
while True:
number = ''.join(random.choices(string.ascii_uppercase + string.digits, k=10))
if not Order.objects.filter(order_number=number).exists():
return number
@property
def total_amount(self):
"""Calculate total order amount"""
return self.subtotal + self.tax_amount + self.shipping_cost - self.discount_amount
@property
def is_paid(self):
"""Check if order is paid"""
return hasattr(self, 'payment') and self.payment.is_successful
@property
def can_be_cancelled(self):
"""Check if order can be cancelled"""
return self.status in ['pending', 'processing']
def get_status_display_class(self):
"""Get CSS class for status display"""
status_classes = {
'pending': 'warning',
'processing': 'info',
'shipped': 'primary',
'delivered': 'success',
'cancelled': 'danger',
'refunded': 'secondary',
}
return status_classes.get(self.status, 'secondary')
def mark_as_shipped(self):
"""Mark order as shipped"""
if self.status == 'processing':
self.status = 'shipped'
self.shipped_at = timezone.now()
self.save()
def mark_as_delivered(self):
"""Mark order as delivered"""
if self.status == 'shipped':
self.status = 'delivered'
self.delivered_at = timezone.now()
self.save()
Django models provide the foundation for your application's data layer. Understanding field types, relationships, and custom model methods enables you to create robust, maintainable data structures that accurately represent your business logic while leveraging Django's powerful ORM capabilities.
Security Considerations for Forms
Form security is critical for protecting applications from various attacks and ensuring data integrity. This chapter covers comprehensive security measures, from CSRF protection to input validation and advanced security patterns.
Understanding Django Models
Django models are the single, definitive source of information about your data. They contain the essential fields and behaviors of the data you're storing. Each model maps to a single database table, and each attribute of the model represents a database field.