Models and Databases

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.

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.

Model Architecture and Design Principles

The Model-Database Relationship

# 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

Model Inheritance Patterns

Abstract Base Classes

# 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

Multi-table Inheritance

# 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

Proxy Models

# 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

Model Meta Options

Comprehensive Meta Configuration

# 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']),
        ]

Custom Model Managers

Advanced Manager Patterns

# 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()

Model Validation and Clean Methods

Comprehensive Validation Patterns

# 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.