Models and Databases

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.

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.

What Are Django Models?

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.

Key Benefits of Django Models

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

Basic Model Structure

Simple Blog Model

# 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

Model Field Types

Comprehensive Field Examples

# 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}'

Model Relationships

Foreign Keys and One-to-Many

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

Many-to-Many Relationships

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

Model Methods and Properties

Custom Model Behavior

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