Django Models and Database Relationships

Master Django ORM with advanced model relationships, database optimization, and best practices for building scalable Django applications.

S

StalkTechie

Author

December 27, 2024
873 views

Django Models and Database Relationships

Django's Object-Relational Mapping (ORM) system is one of its most powerful features, allowing developers to work with databases using Python objects. In this comprehensive guide, we'll explore advanced model techniques, database relationships, optimization strategies, and best practices for building scalable Django applications.

Advanced Model Fields and Options

Common Field Types


from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
from django.utils import timezone
from django.utils.text import slugify  # Assuming this is imported for slug generation

class Product(models.Model):
    # CharField with choices
    PRODUCT_TYPES = [
        ('physical', 'Physical Product'),
        ('digital', 'Digital Product'),
        ('service', 'Service'),
    ]
    
    name = models.CharField(
        max_length=200,
        help_text="Enter the product name"
    )
    slug = models.SlugField(
        max_length=200,
        unique=True,
        help_text="URL-friendly version of the name"
    )
    product_type = models.CharField(
        max_length=20,
        choices=PRODUCT_TYPES,
        default='physical'
    )
    
    # Numeric fields with validation
    price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        validators=[MinValueValidator(0)]
    )
    discount_percentage = models.PositiveSmallIntegerField(
        default=0,
        validators=[MaxValueValidator(100)],
        help_text="Discount percentage (0-100)"
    )
    
    # Boolean and date fields
    is_active = models.BooleanField(default=True)
    is_featured = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    published_at = models.DateTimeField(null=True, blank=True)
    
    # Text fields
    description = models.TextField(blank=True)
    short_description = models.CharField(max_length=500, blank=True)
    
    # File and image fields
    image = models.ImageField(
        upload_to='products/%Y/%m/%d/',
        null=True,
        blank=True,
        help_text="Product main image"
    )
    documentation = models.FileField(
        upload_to='product_docs/%Y/%m/%d/',
        null=True,
        blank=True
    )
    
    # Custom methods
    @property
    def discounted_price(self):
        if self.discount_percentage > 0:
            discount_amount = (self.price * self.discount_percentage) / 100
            return self.price - discount_amount
        return self.price
    
    @property
    def is_published(self):
        return self.published_at is not None and self.published_at <= timezone.now()
    
    def save(self, *args, **kwargs):
        # Auto-generate slug from name if not provided
        if not self.slug:
            self.slug = slugify(self.name)
        
        # Set published_at if product is being published
        if self.is_active and not self.published_at:
            self.published_at = timezone.now()
        
        super().save(*args, **kwargs)
    
    class Meta:
        ordering = ['-created_at']
        verbose_name = "Product"
        verbose_name_plural = "Products"
        indexes = [
            models.Index(fields=['is_active', 'published_at']),
            models.Index(fields=['product_type', 'is_active']),
            models.Index(fields=['price']),
        ]
        constraints = [
            models.CheckConstraint(
                check=models.Q(price__gte=0),
                name="price_positive"
            ),
        ]
    
    def __str__(self):
        return self.name
    

Advanced Field Options

Django models support a variety of advanced field options and custom fields for more complex scenarios:

  • JSONField: Store JSON data (available in PostgreSQL, SQLite, MySQL, etc.). Example: metadata = models.JSONField(default=dict)
  • ArrayField (PostgreSQL only): Store arrays. Example: tags = ArrayField(models.CharField(max_length=50), size=10)
  • HStoreField (PostgreSQL): Key-value store.
  • UUIDField: For unique identifiers. Example: id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
  • Custom Validators: Use RegexValidator for patterns, e.g., SKU fields.
  • db_column and db_tablespace: Control database-level names and storage.

from django.contrib.postgres.fields import ArrayField, JSONField
import uuid

class AdvancedProduct(models.Model):
    sku = models.CharField(
        max_length=50,
        unique=True,
        validators=[RegexValidator(r'^[A-Z0-9]{10}$', 'SKU must be 10 alphanumeric chars')]
    )
    metadata = models.JSONField(default=dict, blank=True)
    tags = ArrayField(models.CharField(max_length=30), blank=True, default=list)
    unique_id = models.UUIDField(default=uuid.uuid4, editable=False)
    

Model Meta Options

The Meta class allows fine-grained control:

  • abstract = True: For base classes not creating tables.
  • proxy = True: Proxy models for altering behavior without new tables.
  • unique_together or constraints.UniqueConstraint: Enforce uniqueness.
  • permissions: Custom permissions.
  • db_table: Custom table name.

class Meta:
    unique_together = ['name', 'product_type']
    permissions = [('can_publish', 'Can publish products')]
    

Database Relationships

Django supports three main relationship types: ForeignKey (many-to-one), ManyToManyField (many-to-many), and OneToOneField (one-to-one).

ForeignKey (Many-to-One)

Links one record to another model's instance. Use on_delete to handle deletions.


class Category(models.Model):
    name = models.CharField(max_length=100)
    slug = models.SlugField(unique=True)

    def __str__(self):
        return self.name

class Product(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.CASCADE,  # or PROTECT, SET_NULL, etc.
        related_name='products',
        help_text="Product category"
    )
    # ... other fields
    

Access related objects: product.category (forward), category.products.all() (reverse).

ManyToManyField

For many-to-many relations, optionally with through models for extra data.


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

class Product(models.Model):
    tags = models.ManyToManyField(Tag, related_name='products', blank=True)

# With through model
class ProductTag(models.Model):
    product = models.ForeignKey(Product, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    added_at = models.DateTimeField(auto_now_add=True)

class Product(models.Model):
    tags = models.ManyToManyField(Tag, through='ProductTag', related_name='products')
    

OneToOneField

Extends another model, like profiles.


from django.contrib.auth.models import User

class UserProfile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
    bio = models.TextField(blank=True)
    avatar = models.ImageField(upload_to='avatars/')
    

Self-Referential Relationships

For hierarchies, e.g., categories.


class Category(models.Model):
    name = models.CharField(max_length=100)
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, related_name='children')
    

Use libraries like django-mptt or django-treebeard for tree structures.

Query Optimization and Advanced ORM Usage

Selecting and Prefetching Related Data

Avoid N+1 queries with select_related (ForeignKey/OneToOne) and prefetch_related (ManyToMany/reverse FK).


# Efficient query
products = Product.objects.select_related('category').prefetch_related('tags').filter(is_active=True)

for product in products:
    print(product.category.name)  # No extra query
    print([tag.name for tag in product.tags.all()])  # No extra queries
    

Annotations and Aggregations

Use annotate for computed values.


from django.db.models import Count, Avg, F, Q, Value

# Annotate with count and expressions
products = Product.objects.annotate(
    tag_count=Count('tags'),
    final_price=F('price') - (F('price') * F('discount_percentage') / 100),
    is_expensive=Case(
        When(price__gt=100, then=True),
        default=False,
        output_field=BooleanField()
    )
).filter(tag_count__gt=2)
    

Raw SQL and Extra

For complex queries: .raw() or extra() (deprecated, prefer expressions).

Database Transactions and Atomicity


from django.db import transaction

with transaction.atomic():
    product = Product.objects.create(name='Test')
    # Related creations...
    

Model Managers and QuerySets

Custom managers for default querysets.


class ActiveProductManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_active=True)

class Product(models.Model):
    objects = models.Manager()  # Default
    active = ActiveProductManager()

# Usage: Product.active.all()
    

Chainable custom QuerySet methods:


class ProductQuerySet(models.QuerySet):
    def published(self):
        return self.filter(is_published=True)
    
    def with_discount(self):
        return self.filter(discount_percentage__gt=0)

class Product(models.Model):
    objects = ProductQuerySet.as_manager()
    

Migrations and Schema Management

  • Use makemigrations and migrate.
  • Data migrations: RunPython for custom logic.
  • RunSQL for raw SQL.
  • Handle conflicts with --merge.

Best practice: Use django-migration-linter for checks.

Best Practices for Scalable Models

  • Avoid large TextField in lists; use pagination.
  • Index frequently queried fields (e.g., foreign keys, filters).
  • Use defer() or only() to load partial fields.
  • Validate at model level with clean() method.
  • Signals: Use sparingly (post_save, pre_delete); prefer overriding save().
  • Multi-database support: using('db_name').
  • Testing: Use TransactionTestCase for DB checks.
  • Performance: Profile with django-debug-toolbar.

Signals Example


from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Product)
def product_saved(sender, instance, created, **kwargs):
    if created:
        # Send notification or cache invalidate
        pass
    

This guide covers the essentials and advanced topics for Django models. For production, always refer to the official Django documentation for version-specific details (e.g., Django 5.x as of 2025).

Share this post:

Related Articles

Discussion

0 comments

Please log in to join the discussion.

Login to Comment