Django Models and Database Relationships
Master Django ORM with advanced model relationships, database optimization, and best practices for building scalable Django applications.
StalkTechie
Author
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
RegexValidatorfor 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_togetherorconstraints.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
makemigrationsandmigrate. - Data migrations:
RunPythonfor custom logic. RunSQLfor raw SQL.- Handle conflicts with
--merge.
Best practice: Use django-migration-linter for checks.
Best Practices for Scalable Models
- Avoid large
TextFieldin lists; use pagination. - Index frequently queried fields (e.g., foreign keys, filters).
- Use
defer()oronly()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
TransactionTestCasefor 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).