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.
# models.py
from django.db import models
class Article(models.Model):
"""
Each model class inherits from models.Model
Each attribute represents a database field
Django automatically creates a primary key field if not specified
"""
title = models.CharField(max_length=200) # VARCHAR(200)
content = models.TextField() # TEXT
created_at = models.DateTimeField(auto_now_add=True) # DATETIME
is_published = models.BooleanField(default=False) # BOOLEAN
# Django automatically adds:
# id = models.AutoField(primary_key=True) # INTEGER PRIMARY KEY
# models.py
from django.db import models
from django.contrib.auth.models import User
class TimestampedModel(models.Model):
"""Abstract base class for models that need timestamp fields"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True # This model won't create a database table
class SoftDeleteModel(models.Model):
"""Abstract base class for soft delete functionality"""
is_deleted = models.BooleanField(default=False)
deleted_at = models.DateTimeField(null=True, blank=True)
class Meta:
abstract = True
def soft_delete(self):
"""Mark object as deleted without removing from database"""
from django.utils import timezone
self.is_deleted = True
self.deleted_at = timezone.now()
self.save()
def restore(self):
"""Restore soft-deleted object"""
self.is_deleted = False
self.deleted_at = None
self.save()
class AuditableModel(TimestampedModel, SoftDeleteModel):
"""Combines timestamp and soft delete functionality"""
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='%(class)s_created'
)
updated_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='%(class)s_updated'
)
class Meta:
abstract = True
# Using abstract base classes
class BlogPost(AuditableModel):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
content = models.TextField()
# Inherits: created_at, updated_at, is_deleted, deleted_at, created_by, updated_by
class Product(AuditableModel):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=10, decimal_places=2)
sku = models.CharField(max_length=50, unique=True)
# Inherits the same fields as BlogPost
# models.py
from django.db import models
class Person(models.Model):
"""Base person model"""
first_name = models.CharField(max_length=50)
last_name = models.CharField(max_length=50)
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
def __str__(self):
return f'{self.first_name} {self.last_name}'
@property
def full_name(self):
return f'{self.first_name} {self.last_name}'
class Employee(Person):
"""Employee inherits from Person and adds employee-specific fields"""
employee_id = models.CharField(max_length=20, unique=True)
department = models.CharField(max_length=100)
hire_date = models.DateField()
salary = models.DecimalField(max_digits=10, decimal_places=2)
is_active = models.BooleanField(default=True)
class Meta:
verbose_name = 'Employee'
verbose_name_plural = 'Employees'
class Customer(Person):
"""Customer inherits from Person and adds customer-specific fields"""
customer_id = models.CharField(max_length=20, unique=True)
registration_date = models.DateField(auto_now_add=True)
loyalty_points = models.PositiveIntegerField(default=0)
preferred_contact_method = models.CharField(
max_length=20,
choices=[('email', 'Email'), ('phone', 'Phone'), ('sms', 'SMS')],
default='email'
)
class Meta:
verbose_name = 'Customer'
verbose_name_plural = 'Customers'
# Usage:
# employee = Employee.objects.create(
# first_name='John',
# last_name='Doe',
# email='john.doe@company.com',
# employee_id='EMP001',
# department='Engineering',
# hire_date='2023-01-15',
# salary=75000
# )
#
# # Employee has access to Person fields and methods
# print(employee.full_name) # John Doe
# print(employee.department) # Engineering
# models.py
from django.db import models
from django.utils import timezone
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
status = models.CharField(
max_length=20,
choices=[
('draft', 'Draft'),
('published', 'Published'),
('archived', 'Archived')
],
default='draft'
)
created_at = models.DateTimeField(auto_now_add=True)
published_at = models.DateTimeField(null=True, blank=True)
def __str__(self):
return self.title
class PublishedArticleManager(models.Manager):
"""Custom manager for published articles"""
def get_queryset(self):
return super().get_queryset().filter(
status='published',
published_at__lte=timezone.now()
)
class PublishedArticle(Article):
"""Proxy model for published articles with different behavior"""
objects = PublishedArticleManager()
class Meta:
proxy = True
verbose_name = 'Published Article'
verbose_name_plural = 'Published Articles'
def save(self, *args, **kwargs):
# Automatically set published_at when saving
if self.status == 'published' and not self.published_at:
self.published_at = timezone.now()
super().save(*args, **kwargs)
class DraftArticleManager(models.Manager):
"""Custom manager for draft articles"""
def get_queryset(self):
return super().get_queryset().filter(status='draft')
class DraftArticle(Article):
"""Proxy model for draft articles"""
objects = DraftArticleManager()
class Meta:
proxy = True
verbose_name = 'Draft Article'
verbose_name_plural = 'Draft Articles'
def publish(self):
"""Publish the draft article"""
self.status = 'published'
self.published_at = timezone.now()
self.save()
# Usage:
# published_articles = PublishedArticle.objects.all() # Only published articles
# draft_articles = DraftArticle.objects.all() # Only draft articles
#
# draft = DraftArticle.objects.get(id=1)
# draft.publish() # Changes status and sets published_at
# models.py
from django.db import models
class BlogPost(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200)
author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
content = models.TextField()
status = models.CharField(max_length=20, default='draft')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
view_count = models.PositiveIntegerField(default=0)
class Meta:
# Database table name
db_table = 'blog_posts'
# Default ordering
ordering = ['-created_at', 'title']
# Verbose names for admin interface
verbose_name = 'Blog Post'
verbose_name_plural = 'Blog Posts'
# Database indexes for performance
indexes = [
models.Index(fields=['status', 'created_at']),
models.Index(fields=['author', 'status']),
models.Index(fields=['slug']),
]
# Unique constraints
constraints = [
models.UniqueConstraint(
fields=['author', 'slug'],
name='unique_author_slug'
),
models.CheckConstraint(
check=models.Q(view_count__gte=0),
name='positive_view_count'
),
]
# Permissions
permissions = [
('can_publish', 'Can publish blog posts'),
('can_feature', 'Can feature blog posts'),
]
# Default manager name
default_manager_name = 'objects'
# Get latest by field
get_latest_by = 'created_at'
# Required database features
required_db_features = ['supports_json_field']
# Apps that can access this model
app_label = 'blog'
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True)
class Meta:
verbose_name_plural = 'Categories'
unique_together = [['name', 'parent']] # Deprecated, use constraints instead
# Modern approach using constraints
constraints = [
models.UniqueConstraint(
fields=['name', 'parent'],
name='unique_category_name_per_parent'
)
]
class ProductReview(models.Model):
product = models.ForeignKey('Product', on_delete=models.CASCADE)
user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
rating = models.PositiveSmallIntegerField()
comment = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
# Prevent duplicate reviews from same user for same product
constraints = [
models.UniqueConstraint(
fields=['product', 'user'],
name='one_review_per_user_per_product'
),
models.CheckConstraint(
check=models.Q(rating__gte=1) & models.Q(rating__lte=5),
name='rating_range_1_to_5'
)
]
# Composite index for common queries
indexes = [
models.Index(fields=['product', 'rating']),
models.Index(fields=['user', 'created_at']),
]
# models.py
from django.db import models
from django.utils import timezone
class PublishedManager(models.Manager):
"""Manager for published content"""
def get_queryset(self):
return super().get_queryset().filter(
is_published=True,
published_at__lte=timezone.now()
)
def featured(self):
"""Get featured published content"""
return self.get_queryset().filter(is_featured=True)
def by_category(self, category):
"""Get published content by category"""
return self.get_queryset().filter(category=category)
class ArchiveManager(models.Manager):
"""Manager for archived content"""
def get_queryset(self):
return super().get_queryset().filter(is_archived=True)
def by_year(self, year):
"""Get archived content by year"""
return self.get_queryset().filter(created_at__year=year)
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
is_published = models.BooleanField(default=False)
is_featured = models.BooleanField(default=False)
is_archived = models.BooleanField(default=False)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
# Multiple managers
objects = models.Manager() # Default manager
published = PublishedManager() # Published articles only
archived = ArchiveManager() # Archived articles only
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
# Custom QuerySet with chainable methods
class ArticleQuerySet(models.QuerySet):
def published(self):
return self.filter(
is_published=True,
published_at__lte=timezone.now()
)
def featured(self):
return self.filter(is_featured=True)
def by_author(self, author):
return self.filter(author=author)
def recent(self, days=30):
cutoff_date = timezone.now() - timezone.timedelta(days=days)
return self.filter(created_at__gte=cutoff_date)
def search(self, query):
return self.filter(
models.Q(title__icontains=query) |
models.Q(content__icontains=query)
)
class ArticleManager(models.Manager):
def get_queryset(self):
return ArticleQuerySet(self.model, using=self._db)
def published(self):
return self.get_queryset().published()
def featured(self):
return self.get_queryset().featured()
def recent_published(self, days=30):
return self.get_queryset().published().recent(days)
class EnhancedArticle(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey('auth.User', on_delete=models.CASCADE)
is_published = models.BooleanField(default=False)
is_featured = models.BooleanField(default=False)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
objects = ArticleManager()
def __str__(self):
return self.title
# Usage examples:
# EnhancedArticle.objects.published().featured()
# EnhancedArticle.objects.published().by_author(user).recent(7)
# EnhancedArticle.objects.search('django').published()
# models.py
from django.db import models
from django.core.exceptions import ValidationError
from django.utils import timezone
from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
import re
class Event(models.Model):
EVENT_TYPES = [
('conference', 'Conference'),
('workshop', 'Workshop'),
('webinar', 'Webinar'),
('meetup', 'Meetup'),
]
title = models.CharField(max_length=200)
description = models.TextField()
event_type = models.CharField(max_length=20, choices=EVENT_TYPES)
start_datetime = models.DateTimeField()
end_datetime = models.DateTimeField()
max_attendees = models.PositiveIntegerField(
validators=[MinValueValidator(1), MaxValueValidator(10000)]
)
registration_deadline = models.DateTimeField()
is_free = models.BooleanField(default=True)
price = models.DecimalField(
max_digits=8,
decimal_places=2,
null=True,
blank=True,
validators=[MinValueValidator(0)]
)
# Location fields
venue_name = models.CharField(max_length=200, blank=True)
address = models.TextField(blank=True)
online_link = models.URLField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def clean(self):
"""Model-level validation"""
errors = {}
# Validate date relationships
if self.start_datetime and self.end_datetime:
if self.start_datetime >= self.end_datetime:
errors['end_datetime'] = 'End time must be after start time.'
if self.registration_deadline and self.start_datetime:
if self.registration_deadline >= self.start_datetime:
errors['registration_deadline'] = 'Registration deadline must be before event start.'
# Validate future dates
now = timezone.now()
if self.start_datetime and self.start_datetime <= now:
errors['start_datetime'] = 'Event must be scheduled for the future.'
# Validate pricing
if not self.is_free and not self.price:
errors['price'] = 'Price is required for paid events.'
if self.is_free and self.price:
errors['price'] = 'Free events cannot have a price.'
# Validate location based on event type
if self.event_type == 'webinar':
if not self.online_link:
errors['online_link'] = 'Online link is required for webinars.'
if self.venue_name or self.address:
errors['venue_name'] = 'Physical location not needed for webinars.'
else:
if not self.venue_name or not self.address:
errors['venue_name'] = 'Venue name and address required for physical events.'
if errors:
raise ValidationError(errors)
def save(self, *args, **kwargs):
# Run full validation before saving
self.full_clean()
super().save(*args, **kwargs)
def __str__(self):
return self.title
class UserProfile(models.Model):
user = models.OneToOneField('auth.User', on_delete=models.CASCADE)
phone = models.CharField(
max_length=20,
blank=True,
validators=[
RegexValidator(
regex=r'^\+?1?\d{9,15}$',
message='Phone number must be entered in the format: "+999999999". Up to 15 digits allowed.'
)
]
)
birth_date = models.DateField(null=True, blank=True)
bio = models.TextField(max_length=500, blank=True)
website = models.URLField(blank=True)
def clean(self):
"""Custom validation for user profile"""
errors = {}
# Validate birth date
if self.birth_date:
today = timezone.now().date()
age = today.year - self.birth_date.year - (
(today.month, today.day) < (self.birth_date.month, self.birth_date.day)
)
if self.birth_date > today:
errors['birth_date'] = 'Birth date cannot be in the future.'
elif age < 13:
errors['birth_date'] = 'You must be at least 13 years old.'
elif age > 120:
errors['birth_date'] = 'Please enter a valid birth date.'
# Validate bio content
if self.bio:
# Check for inappropriate content
inappropriate_words = ['spam', 'scam', 'fake', 'phishing']
bio_lower = self.bio.lower()
for word in inappropriate_words:
if word in bio_lower:
errors['bio'] = f'Bio contains inappropriate content: "{word}"'
break
# Check for excessive links
url_pattern = r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+'
urls = re.findall(url_pattern, self.bio)
if len(urls) > 2:
errors['bio'] = 'Bio cannot contain more than 2 URLs.'
if errors:
raise ValidationError(errors)
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
@property
def age(self):
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)
)
# Custom field validation
def validate_even_number(value):
"""Custom validator for even numbers"""
if value % 2 != 0:
raise ValidationError(f'{value} is not an even number.')
def validate_file_size(value):
"""Custom validator for file size (max 5MB)"""
filesize = value.size
if filesize > 5 * 1024 * 1024: # 5MB
raise ValidationError('File size cannot exceed 5MB.')
class Document(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(
upload_to='documents/',
validators=[validate_file_size]
)
page_count = models.PositiveIntegerField(
validators=[validate_even_number],
help_text='Must be an even number for proper printing.'
)
created_at = models.DateTimeField(auto_now_add=True)
def clean(self):
"""Additional file validation"""
if self.file:
# Validate file extension
allowed_extensions = ['.pdf', '.doc', '.docx']
file_extension = self.file.name.lower().split('.')[-1]
if f'.{file_extension}' not in allowed_extensions:
raise ValidationError({
'file': f'File type .{file_extension} is not allowed. Allowed types: {", ".join(allowed_extensions)}'
})
def save(self, *args, **kwargs):
self.full_clean()
super().save(*args, **kwargs)
Understanding Django models deeply enables you to create robust, well-validated data structures that accurately represent your business logic while maintaining data integrity and providing excellent performance through proper indexing and constraints.
Models and Databases
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.
Defining Fields
Django model fields are the building blocks of your data structure. Each field type corresponds to a specific database column type and provides validation, conversion, and rendering capabilities. Understanding field options and customization is crucial for creating robust data models.