Models and Databases

Relationships and Foreign Keys

Django's ORM provides powerful tools for defining and working with relationships between models. Understanding how to properly design and implement relationships is crucial for creating efficient, maintainable database schemas.

Relationships and Foreign Keys

Django's ORM provides powerful tools for defining and working with relationships between models. Understanding how to properly design and implement relationships is crucial for creating efficient, maintainable database schemas.

One-to-One Relationships

One-to-one relationships link exactly one record in one table to exactly one record in another table. This is useful for extending models or separating concerns.

Basic One-to-One Implementation

# models.py
from django.db import models
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError

class UserProfile(models.Model):
    """Extends the built-in User model with additional fields"""
    
    user = models.OneToOneField(
        User,
        on_delete=models.CASCADE,
        related_name='profile'
    )
    
    # Additional profile fields
    bio = models.TextField(max_length=500, blank=True)
    birth_date = models.DateField(null=True, blank=True)
    avatar = models.ImageField(upload_to='avatars/', blank=True, null=True)
    phone = models.CharField(max_length=20, blank=True)
    website = models.URLField(blank=True)
    
    # Social media links
    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}'s Profile"
    
    @property
    def full_name(self):
        return self.user.get_full_name() or self.user.username
    
    @property
    def age(self):
        if self.birth_date:
            from django.utils import timezone
            today = timezone.now().date()
            return today.year - self.birth_date.year - (
                (today.month, today.day) < (self.birth_date.month, self.birth_date.day)
            )
        return None

# Signal to automatically create profile when user is created
from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    if created:
        UserProfile.objects.create(user=instance)

@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
    if hasattr(instance, 'profile'):
        instance.profile.save()

# Usage examples:
# user = User.objects.get(username='john')
# profile = user.profile  # Access via related_name
# user_from_profile = profile.user  # Reverse access

Advanced One-to-One Patterns

# models.py
from django.db import models

class Company(models.Model):
    name = models.CharField(max_length=200)
    founded_date = models.DateField()
    
    def __str__(self):
        return self.name

class CompanySettings(models.Model):
    """Company-specific configuration settings"""
    
    company = models.OneToOneField(
        Company,
        on_delete=models.CASCADE,
        related_name='settings'
    )
    
    # Business settings
    business_hours_start = models.TimeField(default='09:00')
    business_hours_end = models.TimeField(default='17:00')
    timezone = models.CharField(max_length=50, default='UTC')
    
    # Notification settings
    email_notifications = models.BooleanField(default=True)
    sms_notifications = models.BooleanField(default=False)
    
    # API settings
    api_rate_limit = models.PositiveIntegerField(default=1000)
    api_key = models.CharField(max_length=64, unique=True, editable=False)
    
    def save(self, *args, **kwargs):
        if not self.api_key:
            import secrets
            self.api_key = secrets.token_hex(32)
        super().save(*args, **kwargs)

class Employee(models.Model):
    company = models.ForeignKey(Company, on_delete=models.CASCADE)
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField(unique=True)
    
    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class EmployeeDetails(models.Model):
    """Extended employee information"""
    
    employee = models.OneToOneField(
        Employee,
        on_delete=models.CASCADE,
        related_name='details'
    )
    
    # Personal details
    employee_id = models.CharField(max_length=20, unique=True)
    hire_date = models.DateField()
    department = models.CharField(max_length=100)
    position = models.CharField(max_length=100)
    salary = models.DecimalField(max_digits=10, decimal_places=2)
    
    # Emergency contact
    emergency_contact_name = models.CharField(max_length=100)
    emergency_contact_phone = models.CharField(max_length=20)
    
    # Benefits
    health_insurance = models.BooleanField(default=True)
    dental_insurance = models.BooleanField(default=False)
    retirement_plan = models.BooleanField(default=True)
    
    def __str__(self):
        return f"Details for {self.employee}"

One-to-Many Relationships (Foreign Keys)

One-to-many relationships are the most common type of relationship, where one record can be related to multiple records in another table.

Basic Foreign Key Implementation

# models.py
from django.db import models
from django.contrib.auth.models import User

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)
    parent = models.ForeignKey(
        'self',  # Self-referential foreign key
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='subcategories'
    )
    
    class Meta:
        verbose_name_plural = 'Categories'
        ordering = ['name']
    
    def __str__(self):
        return self.name
    
    @property
    def is_root_category(self):
        return self.parent is None
    
    def get_ancestors(self):
        """Get all parent categories"""
        ancestors = []
        current = self.parent
        while current:
            ancestors.append(current)
            current = current.parent
        return ancestors
    
    def get_descendants(self):
        """Get all child categories recursively"""
        descendants = []
        for child in self.subcategories.all():
            descendants.append(child)
            descendants.extend(child.get_descendants())
        return descendants

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)
    
    # Foreign key to User model
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='posts'  # Allows user.posts.all()
    )
    
    # Foreign key to Category model
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,  # Keep posts if category is deleted
        null=True,
        blank=True,
        related_name='posts'
    )
    
    content = models.TextField()
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='draft')
    
    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=['status', 'published_at']),
            models.Index(fields=['author', 'status']),
            models.Index(fields=['category', 'status']),
        ]
    
    def __str__(self):
        return self.title

class Comment(models.Model):
    # Foreign key to Post
    post = models.ForeignKey(
        Post,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    
    # Foreign key to User
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='comments'
    )
    
    # Self-referential foreign key for nested comments
    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)
    
    class Meta:
        ordering = ['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
    
    def get_replies(self):
        """Get all replies to this comment"""
        return self.replies.filter(is_approved=True)

# Usage examples:
# Get all posts by an author
# author_posts = user.posts.all()

# Get all posts in a category
# category_posts = category.posts.filter(status='published')

# Get all comments for a post
# post_comments = post.comments.filter(is_approved=True)

# Get all replies to a comment
# comment_replies = comment.replies.all()

Foreign Key Options and Behaviors

# models.py
from django.db import models

class Publisher(models.Model):
    name = models.CharField(max_length=200)
    address = models.TextField()
    
    def __str__(self):
        return self.name

class Author(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField()
    
    def __str__(self):
        return self.name

class Book(models.Model):
    title = models.CharField(max_length=200)
    isbn = models.CharField(max_length=13, unique=True)
    
    # CASCADE: Delete books when publisher is deleted
    publisher = models.ForeignKey(
        Publisher,
        on_delete=models.CASCADE,
        related_name='books'
    )
    
    # SET_NULL: Keep books but set author to NULL when author is deleted
    author = models.ForeignKey(
        Author,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='books'
    )
    
    # PROTECT: Prevent deletion of referenced object
    primary_category = models.ForeignKey(
        'Category',
        on_delete=models.PROTECT,
        related_name='primary_books'
    )
    
    # SET_DEFAULT: Set to default value when referenced object is deleted
    backup_publisher = models.ForeignKey(
        Publisher,
        on_delete=models.SET_DEFAULT,
        default=1,  # Must have a publisher with id=1
        related_name='backup_books'
    )
    
    publication_date = models.DateField()
    pages = models.PositiveIntegerField()
    
    def __str__(self):
        return self.title

# Custom deletion behavior
def get_default_author():
    """Get or create a default 'Unknown Author'"""
    author, created = Author.objects.get_or_create(
        name='Unknown Author',
        defaults={'email': 'unknown@example.com'}
    )
    return author.pk

class Article(models.Model):
    title = models.CharField(max_length=200)
    
    # SET: Call a function when referenced object is deleted
    author = models.ForeignKey(
        Author,
        on_delete=models.SET(get_default_author),
        related_name='articles'
    )
    
    content = models.TextField()
    
    def __str__(self):
        return self.title

class Order(models.Model):
    customer = models.ForeignKey(
        'Customer',
        on_delete=models.CASCADE,
        related_name='orders'
    )
    
    order_date = models.DateTimeField(auto_now_add=True)
    total_amount = models.DecimalField(max_digits=10, decimal_places=2)
    
    def __str__(self):
        return f'Order #{self.pk}'

class OrderItem(models.Model):
    order = models.ForeignKey(
        Order,
        on_delete=models.CASCADE,
        related_name='items'
    )
    
    product = models.ForeignKey(
        'Product',
        on_delete=models.CASCADE,
        related_name='order_items'
    )
    
    quantity = models.PositiveIntegerField()
    unit_price = models.DecimalField(max_digits=8, decimal_places=2)
    
    @property
    def total_price(self):
        return self.quantity * self.unit_price
    
    def __str__(self):
        return f'{self.quantity}x {self.product.name}'

Many-to-Many Relationships

Many-to-many relationships allow multiple records in one table to be related to multiple records in another table.

Simple Many-to-Many

# models.py
from django.db import models

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
    
    def __str__(self):
        return self.name

class Article(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    
    # Simple many-to-many relationship
    tags = models.ManyToManyField(
        Tag,
        blank=True,
        related_name='articles',
        help_text='Select relevant tags'
    )
    
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.title
    
    def get_tag_names(self):
        """Get comma-separated list of tag names"""
        return ', '.join(tag.name for tag in self.tags.all())

# Usage examples:
# Add tags to an article
# article = Article.objects.get(id=1)
# tag1 = Tag.objects.get(name='Django')
# tag2 = Tag.objects.get(name='Python')
# article.tags.add(tag1, tag2)

# Remove tags
# article.tags.remove(tag1)

# Set tags (replaces all existing tags)
# article.tags.set([tag1, tag2])

# Get all articles with a specific tag
# django_articles = Tag.objects.get(name='Django').articles.all()

# Get articles with multiple tags
# articles_with_both_tags = Article.objects.filter(
#     tags__name='Django'
# ).filter(tags__name='Python')

Many-to-Many with Through Model

# models.py
from django.db import models
from django.contrib.auth.models import User

class Project(models.Model):
    name = models.CharField(max_length=200)
    description = models.TextField()
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    
    def __str__(self):
        return self.name

class Skill(models.Model):
    name = models.CharField(max_length=100, unique=True)
    category = models.CharField(max_length=50)
    
    def __str__(self):
        return self.name

class Person(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    bio = models.TextField(blank=True)
    
    # Many-to-many with through model
    projects = models.ManyToManyField(
        Project,
        through='ProjectMembership',
        related_name='members'
    )
    
    skills = models.ManyToManyField(
        Skill,
        through='PersonSkill',
        related_name='people'
    )
    
    def __str__(self):
        return self.user.get_full_name() or self.user.username

class ProjectMembership(models.Model):
    """Through model for Person-Project relationship"""
    
    ROLE_CHOICES = [
        ('manager', 'Project Manager'),
        ('developer', 'Developer'),
        ('designer', 'Designer'),
        ('tester', 'Tester'),
        ('analyst', 'Business Analyst'),
    ]
    
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    
    role = models.CharField(max_length=20, choices=ROLE_CHOICES)
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    hours_per_week = models.PositiveSmallIntegerField(default=40)
    is_active = models.BooleanField(default=True)
    
    class Meta:
        unique_together = [['person', 'project', 'role']]
        ordering = ['start_date']
    
    def __str__(self):
        return f'{self.person} - {self.project} ({self.role})'
    
    @property
    def is_current(self):
        from django.utils import timezone
        today = timezone.now().date()
        return (
            self.is_active and
            self.start_date <= today and
            (self.end_date is None or self.end_date >= today)
        )

class PersonSkill(models.Model):
    """Through model for Person-Skill relationship"""
    
    PROFICIENCY_CHOICES = [
        (1, 'Beginner'),
        (2, 'Intermediate'),
        (3, 'Advanced'),
        (4, 'Expert'),
    ]
    
    person = models.ForeignKey(Person, on_delete=models.CASCADE)
    skill = models.ForeignKey(Skill, on_delete=models.CASCADE)
    
    proficiency_level = models.PositiveSmallIntegerField(choices=PROFICIENCY_CHOICES)
    years_of_experience = models.PositiveSmallIntegerField(default=0)
    is_certified = models.BooleanField(default=False)
    acquired_date = models.DateField()
    
    class Meta:
        unique_together = [['person', 'skill']]
    
    def __str__(self):
        return f'{self.person} - {self.skill} ({self.get_proficiency_level_display()})'

# Usage with through models:
# Create membership
# membership = ProjectMembership.objects.create(
#     person=person,
#     project=project,
#     role='developer',
#     start_date='2023-01-01',
#     hours_per_week=40
# )

# Query through the relationship
# current_memberships = ProjectMembership.objects.filter(
#     person=person,
#     is_active=True
# )

# Get all developers on a project
# developers = ProjectMembership.objects.filter(
#     project=project,
#     role='developer',
#     is_active=True
# )

Advanced Many-to-Many Patterns

# models.py
from django.db import models

class Course(models.Model):
    name = models.CharField(max_length=200)
    code = models.CharField(max_length=10, unique=True)
    credits = models.PositiveSmallIntegerField()
    
    def __str__(self):
        return f'{self.code}: {self.name}'

class Student(models.Model):
    student_id = models.CharField(max_length=20, unique=True)
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField(unique=True)
    
    # Many-to-many with additional enrollment data
    courses = models.ManyToManyField(
        Course,
        through='Enrollment',
        related_name='students'
    )
    
    def __str__(self):
        return f'{self.first_name} {self.last_name} ({self.student_id})'
    
    def get_current_courses(self):
        """Get currently enrolled courses"""
        from django.utils import timezone
        current_semester = timezone.now().year  # Simplified
        return self.courses.filter(
            enrollment__semester_year=current_semester,
            enrollment__is_active=True
        )
    
    def get_gpa(self):
        """Calculate GPA from completed courses"""
        completed_enrollments = Enrollment.objects.filter(
            student=self,
            grade__isnull=False
        )
        
        if not completed_enrollments:
            return None
        
        total_points = 0
        total_credits = 0
        
        grade_points = {'A': 4.0, 'B': 3.0, 'C': 2.0, 'D': 1.0, 'F': 0.0}
        
        for enrollment in completed_enrollments:
            points = grade_points.get(enrollment.grade, 0.0)
            credits = enrollment.course.credits
            total_points += points * credits
            total_credits += credits
        
        return total_points / total_credits if total_credits > 0 else 0.0

class Enrollment(models.Model):
    """Through model for Student-Course relationship"""
    
    GRADE_CHOICES = [
        ('A', 'A'),
        ('B', 'B'),
        ('C', 'C'),
        ('D', 'D'),
        ('F', 'F'),
        ('I', 'Incomplete'),
        ('W', 'Withdrawn'),
    ]
    
    student = models.ForeignKey(Student, on_delete=models.CASCADE)
    course = models.ForeignKey(Course, on_delete=models.CASCADE)
    
    # Enrollment details
    semester_year = models.PositiveIntegerField()
    semester_term = models.CharField(
        max_length=10,
        choices=[('fall', 'Fall'), ('spring', 'Spring'), ('summer', 'Summer')]
    )
    
    enrollment_date = models.DateField(auto_now_add=True)
    is_active = models.BooleanField(default=True)
    
    # Grading
    grade = models.CharField(max_length=2, choices=GRADE_CHOICES, null=True, blank=True)
    grade_date = models.DateField(null=True, blank=True)
    
    # Attendance tracking
    classes_attended = models.PositiveIntegerField(default=0)
    total_classes = models.PositiveIntegerField(default=0)
    
    class Meta:
        unique_together = [['student', 'course', 'semester_year', 'semester_term']]
        ordering = ['-semester_year', 'semester_term']
    
    def __str__(self):
        return f'{self.student} enrolled in {self.course} ({self.semester_term} {self.semester_year})'
    
    @property
    def attendance_percentage(self):
        if self.total_classes > 0:
            return (self.classes_attended / self.total_classes) * 100
        return 0
    
    def is_passing(self):
        """Check if student is passing the course"""
        passing_grades = ['A', 'B', 'C', 'D']
        return self.grade in passing_grades if self.grade else None

# Self-referential many-to-many
class Person(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    
    # Many-to-many relationship with itself
    friends = models.ManyToManyField(
        'self',
        through='Friendship',
        symmetrical=False,  # Friendship might not be mutual
        related_name='friend_of'
    )
    
    def __str__(self):
        return self.name

class Friendship(models.Model):
    """Through model for Person-Person friendship"""
    
    from_person = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        related_name='friendships_initiated'
    )
    to_person = models.ForeignKey(
        Person,
        on_delete=models.CASCADE,
        related_name='friendships_received'
    )
    
    created_date = models.DateField(auto_now_add=True)
    is_confirmed = models.BooleanField(default=False)
    
    class Meta:
        unique_together = [['from_person', 'to_person']]
    
    def __str__(self):
        status = 'confirmed' if self.is_confirmed else 'pending'
        return f'{self.from_person} -> {self.to_person} ({status})'

Understanding Django relationships enables you to model complex real-world data structures efficiently while maintaining data integrity and providing intuitive APIs for accessing related data.