Cross-Database Relations
When using multi-database setup, ForeignKeys to models in different databases require special handling.
The Problem
Consider this scenario:
- User model lives in
defaultdatabase - Post model lives in
blog_dbdatabase - Post needs a ForeignKey to User
Without db_constraint=False
# ❌ This WILL FAIL during migrations
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(User, on_delete=models.CASCADE)Error:
django.db.utils.ProgrammingError: relation "auth_user" does not existWhy? Django tries to create a database-level foreign key constraint in blog_db, but the User table only exists in default database.
The Solution: db_constraint=False
# ✅ CORRECT - Works with multi-database
from django.contrib.auth import get_user_model
User = get_user_model()
class Post(models.Model):
title = models.CharField(max_length=200)
# Cross-database ForeignKey
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_posts',
db_constraint=False # ⚠️ REQUIRED for cross-database FK
)
class Meta:
db_table = 'blog_post'How It Works
What db_constraint=False Does
- ✅ Prevents database-level foreign key constraint
- ✅ Django ORM still validates relationships
- ✅
post.authorstill returns User object - ✅
user.blog_posts.all()still works - ❌ No referential integrity at database level
- ❌ No cascade deletes at database level
Django Handles It
Django manages the relationship at the application level instead of database level:
# This works - Django fetches from both databases
post = Post.objects.get(id=1) # From blog_db
author = post.author # Django fetches from default database
# Reverse relation also works
user = User.objects.get(id=1) # From default
posts = user.blog_posts.all() # Django fetches from blog_dbComplete Examples
Blog App
# apps/blog/models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model() # Lives in 'default' database
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
# Cross-database ForeignKey to User
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_posts',
db_constraint=False # REQUIRED
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'blog_post'
class Comment(models.Model):
post = models.ForeignKey(
Post,
on_delete=models.CASCADE,
related_name='comments',
# No db_constraint=False needed - same database
)
# Cross-database ForeignKey to User
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='blog_comments',
db_constraint=False # REQUIRED
)
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)Shop App
# apps/shop/models.py
from django.db import models
from django.contrib.auth import get_user_model
from decimal import Decimal
User = get_user_model()
class Order(models.Model):
order_number = models.CharField(max_length=50, unique=True)
# Cross-database ForeignKey to User
customer = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='orders',
db_constraint=False # REQUIRED
)
total = models.DecimalField(max_digits=10, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'shop_order'
class Product(models.Model):
name = models.CharField(max_length=200)
price = models.DecimalField(max_digits=10, decimal_places=2)
# Same database - no db_constraint=False needed
category = models.ForeignKey(
'Category',
on_delete=models.CASCADE,
)Usage Examples
Creating Objects
from django.contrib.auth import get_user_model
from apps.blog.models import Post
from apps.shop.models import Order
User = get_user_model()
# Create user in default database
user = User.objects.create_user(
username='john',
email='[email protected]',
password='secure_password'
)
# Create post in blog_db with cross-database FK
post = Post.objects.create(
title='Hello World',
content='Multi-database setup works!',
author=user # Cross-database FK works!
)
# Create order in shop_db with cross-database FK
order = Order.objects.create(
order_number='ORD-001',
customer=user, # Cross-database FK works!
total=Decimal('99.99')
)Accessing Relationships
# Forward relationship (Post → User)
post = Post.objects.get(id=1)
print(post.author.username) # Django fetches from default database
# Reverse relationship (User → Posts)
user = User.objects.get(id=1)
posts = user.blog_posts.all() # Django fetches from blog_db
# Reverse relationship (User → Orders)
orders = user.orders.all() # Django fetches from shop_dbTrade-offs
What You Gain
✅ Multi-database architecture
- Separate databases for different domains
- Independent scaling
- Data isolation
✅ Django ORM works normally
post.authorreturns User objectuser.blog_posts.all()returns QuerySet- Django validates relationships
What You Lose
❌ No database-level integrity
- Database doesn’t enforce foreign keys
- Can have orphaned records
- No automatic referential integrity
❌ No database-level cascades
ON DELETE CASCADEdoesn’t work at DB level- Django handles deletes at application level
- Requires extra queries
Handling Data Integrity
1. Django Signals for Cascade Deletes
# apps/blog/signals.py
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from django.contrib.auth import get_user_model
User = get_user_model()
@receiver(pre_delete, sender=User)
def delete_user_content(sender, instance, **kwargs):
"""Delete user's content when user is deleted."""
# Delete blog posts
instance.blog_posts.all().delete()
# Delete comments
instance.blog_comments.all().delete()
# Delete orders
instance.orders.all().delete()# apps/blog/apps.py
class BlogConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.blog'
def ready(self):
import apps.blog.signals # Register signals2. Periodic Cleanup Job
# apps/blog/management/commands/cleanup_orphaned.py
from django.core.management.base import BaseCommand
from django.contrib.auth import get_user_model
from apps.blog.models import Post, Comment
User = get_user_model()
class Command(BaseCommand):
help = 'Remove orphaned blog records'
def handle(self, *args, **options):
# Get all user IDs
valid_user_ids = set(User.objects.values_list('id', flat=True))
# Find orphaned posts
orphaned_posts = Post.objects.exclude(author_id__in=valid_user_ids)
count = orphaned_posts.count()
orphaned_posts.delete()
self.stdout.write(
self.style.SUCCESS(f'Deleted {count} orphaned posts')
)
# Find orphaned comments
orphaned_comments = Comment.objects.exclude(author_id__in=valid_user_ids)
count = orphaned_comments.count()
orphaned_comments.delete()
self.stdout.write(
self.style.SUCCESS(f'Deleted {count} orphaned comments')
)3. Application-Level Validation
# apps/blog/views.py
from django.core.exceptions import ValidationError
def create_post(request):
user_id = request.POST.get('author_id')
# Validate user exists before creating post
if not User.objects.filter(id=user_id).exists():
raise ValidationError("User does not exist")
post = Post.objects.create(
title=request.POST.get('title'),
content=request.POST.get('content'),
author_id=user_id
)
return post4. Database Constraints (When Possible)
For same-database relationships, use normal ForeignKeys:
# ✅ Same database - use normal FK with constraints
class Comment(models.Model):
post = models.ForeignKey(
Post,
on_delete=models.CASCADE, # Database enforces this
# No db_constraint=False - both in blog_db
)Performance Considerations
N+1 Query Problem Across Databases
# ❌ Bad: N+1 queries across databases
posts = Post.objects.all()
for post in posts:
print(post.author.username) # Separate query to default db for each post# ✅ Better: Manual prefetch
posts = Post.objects.all()
user_ids = [p.author_id for p in posts]
users = {u.id: u for u in User.objects.filter(id__in=user_ids)}
for post in posts:
post._author_cache = users[post.author_id]
print(post._author_cache.username)select_related Doesn’t Work
# ❌ select_related doesn't work across databases
posts = Post.objects.select_related('author').all()
# Author will still be fetched separatelyCache Cross-Database Lookups
from django.core.cache import cache
def get_post_with_author(post_id):
cache_key = f'post_author_{post_id}'
author = cache.get(cache_key)
if not author:
post = Post.objects.get(id=post_id)
author = post.author
cache.set(cache_key, author, timeout=3600)
return authorWhen db_constraint=False Is Required
Required
✅ ForeignKey from routed app to default database:
# Post in blog_db → User in default
author = models.ForeignKey(User, ..., db_constraint=False)NOT Required
❌ ForeignKey within same database:
# Comment in blog_db → Post in blog_db
post = models.ForeignKey(Post, on_delete=models.CASCADE)
# Normal FK - both in blog_db❌ ForeignKey in default database:
# UserProfile in default → User in default
user = models.ForeignKey(User, on_delete=models.CASCADE)
# Normal FK - both in defaultMigration Example
# apps/blog/migrations/0001_initial.py
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='Post',
fields=[
('id', models.BigAutoField(primary_key=True)),
('title', models.CharField(max_length=200)),
('content', models.TextField()),
('author', models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='blog_posts',
to='auth.user',
db_constraint=False, # Note: db_constraint=False
)),
],
),
]Troubleshooting
Error: relation “auth_user” does not exist
Problem: Missing db_constraint=False
Solution:
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
db_constraint=False # Add this
)Orphaned Records
Check for orphaned records:
# Find posts with missing authors
from apps.blog.models import Post
from django.contrib.auth import get_user_model
User = get_user_model()
valid_user_ids = User.objects.values_list('id', flat=True)
orphaned = Post.objects.exclude(author_id__in=valid_user_ids)
print(f"Orphaned posts: {orphaned.count()}")See Also
- Multi-Database - Multi-database setup
- Routing - Database routing system
- Migrations - Migration commands
- Multi-Database Guide - Complete guide
- Sample Project - Working example