891 lines
24 KiB
Markdown
891 lines
24 KiB
Markdown
|
|
# Django Development
|
|
|
|
## Overview
|
|
|
|
**Django development specialist covering Django ORM optimization, DRF best practices, caching strategies, migrations, testing, and production deployment.**
|
|
|
|
**Core principle**: Django's "batteries included" philosophy is powerful but requires understanding which battery to use when - master Django's tools to avoid reinventing wheels or choosing wrong patterns.
|
|
|
|
## When to Use This Skill
|
|
|
|
Use when encountering:
|
|
|
|
- **ORM optimization**: N+1 queries, select_related vs prefetch_related, query performance
|
|
- **DRF patterns**: Serializers, ViewSets, permissions, nested relationships
|
|
- **Caching**: Cache framework, per-view caching, template fragment caching
|
|
- **Migrations**: Zero-downtime migrations, data migrations, squashing
|
|
- **Testing**: Django TestCase, fixtures, factories, mocking
|
|
- **Deployment**: Gunicorn, static files, database pooling
|
|
- **Async Django**: Channels, async views, WebSockets
|
|
- **Admin customization**: Custom admin actions, list filters, inlines
|
|
|
|
**Do NOT use for**:
|
|
- General Python patterns (use `axiom-python-engineering`)
|
|
- API design principles (use `rest-api-design`)
|
|
- Database-agnostic patterns (use `database-integration`)
|
|
- Authentication flows (use `api-authentication`)
|
|
|
|
## Django ORM Optimization
|
|
|
|
### select_related vs prefetch_related
|
|
|
|
**Decision matrix**:
|
|
|
|
| Relationship | Method | SQL Strategy | Use When |
|
|
|--------------|--------|--------------|----------|
|
|
| ForeignKey (many-to-one) | `select_related` | JOIN | Book → Author |
|
|
| OneToOneField | `select_related` | JOIN | User → Profile |
|
|
| Reverse ForeignKey (one-to-many) | `prefetch_related` | Separate query + IN | Author → Books |
|
|
| ManyToManyField | `prefetch_related` | Separate query + IN | Book → Tags |
|
|
|
|
**Example - select_related (JOIN)**:
|
|
|
|
```python
|
|
# BAD: N+1 queries (1 + N)
|
|
books = Book.objects.all()
|
|
for book in books:
|
|
print(book.author.name) # Additional query per book
|
|
|
|
# GOOD: Single JOIN query
|
|
books = Book.objects.select_related('author').all()
|
|
for book in books:
|
|
print(book.author.name) # No additional queries
|
|
|
|
# SQL generated:
|
|
# SELECT book.*, author.* FROM book JOIN author ON book.author_id = author.id
|
|
```
|
|
|
|
**Example - prefetch_related (IN query)**:
|
|
|
|
```python
|
|
# BAD: N+1 queries
|
|
authors = Author.objects.all()
|
|
for author in authors:
|
|
print(author.books.count()) # Query per author
|
|
|
|
# GOOD: 2 queries total
|
|
authors = Author.objects.prefetch_related('books').all()
|
|
for author in authors:
|
|
print(author.books.count()) # No additional queries
|
|
|
|
# SQL generated:
|
|
# Query 1: SELECT * FROM author
|
|
# Query 2: SELECT * FROM book WHERE author_id IN (1, 2, 3, ...)
|
|
```
|
|
|
|
**Nested prefetching**:
|
|
|
|
```python
|
|
from django.db.models import Prefetch
|
|
|
|
# Fetch authors → books → reviews (3 queries)
|
|
authors = Author.objects.prefetch_related(
|
|
Prefetch('books', queryset=Book.objects.prefetch_related('reviews'))
|
|
)
|
|
|
|
# Custom filtering on prefetch
|
|
recent_books = Book.objects.filter(
|
|
published_date__gte=timezone.now() - timedelta(days=30)
|
|
).order_by('-published_date')
|
|
|
|
authors = Author.objects.prefetch_related(
|
|
Prefetch('books', queryset=recent_books, to_attr='recent_books')
|
|
)
|
|
|
|
# Access via custom attribute
|
|
for author in authors:
|
|
for book in author.recent_books: # Only recent books
|
|
print(book.title)
|
|
```
|
|
|
|
### Query Debugging
|
|
|
|
```python
|
|
from django.db import connection, reset_queries
|
|
from django.conf import settings
|
|
|
|
# Enable in settings.py: DEBUG = True
|
|
# Or use django-debug-toolbar
|
|
|
|
def debug_queries(func):
|
|
"""Decorator to debug query counts"""
|
|
def wrapper(*args, **kwargs):
|
|
reset_queries()
|
|
result = func(*args, **kwargs)
|
|
print(f"Queries: {len(connection.queries)}")
|
|
for query in connection.queries:
|
|
print(f" {query['time']}s: {query['sql'][:100]}")
|
|
return result
|
|
return wrapper
|
|
|
|
@debug_queries
|
|
def get_books():
|
|
return list(Book.objects.select_related('author').prefetch_related('tags'))
|
|
```
|
|
|
|
**Django Debug Toolbar** (production alternative - django-silk):
|
|
|
|
```python
|
|
# settings.py
|
|
INSTALLED_APPS = [
|
|
'debug_toolbar',
|
|
# ...
|
|
]
|
|
|
|
MIDDLEWARE = [
|
|
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
|
# ...
|
|
]
|
|
|
|
INTERNAL_IPS = ['127.0.0.1']
|
|
|
|
# For production: use django-silk for profiling
|
|
INSTALLED_APPS += ['silk']
|
|
MIDDLEWARE += ['silk.middleware.SilkyMiddleware']
|
|
```
|
|
|
|
### Annotation and Aggregation
|
|
|
|
**Annotate** (add computed fields):
|
|
|
|
```python
|
|
from django.db.models import Count, Avg, Sum, F, Q
|
|
|
|
# Add book count to each author
|
|
authors = Author.objects.annotate(
|
|
book_count=Count('books'),
|
|
avg_rating=Avg('books__rating'),
|
|
total_sales=Sum('books__sales')
|
|
)
|
|
|
|
for author in authors:
|
|
print(f"{author.name}: {author.book_count} books, avg rating {author.avg_rating}")
|
|
```
|
|
|
|
**Aggregate** (single value across queryset):
|
|
|
|
```python
|
|
from django.db.models import Avg
|
|
|
|
# Get average rating across all books
|
|
avg_rating = Book.objects.aggregate(Avg('rating'))
|
|
# Returns: {'rating__avg': 4.2}
|
|
|
|
# Multiple aggregations
|
|
stats = Book.objects.aggregate(
|
|
avg_rating=Avg('rating'),
|
|
total_sales=Sum('sales'),
|
|
book_count=Count('id')
|
|
)
|
|
```
|
|
|
|
**Conditional aggregation with Q**:
|
|
|
|
```python
|
|
from django.db.models import Q, Count
|
|
|
|
# Count books by rating category
|
|
Author.objects.annotate(
|
|
high_rated_books=Count('books', filter=Q(books__rating__gte=4.0)),
|
|
low_rated_books=Count('books', filter=Q(books__rating__lt=3.0))
|
|
)
|
|
```
|
|
|
|
## Django REST Framework Patterns
|
|
|
|
### ViewSet vs APIView
|
|
|
|
**Decision matrix**:
|
|
|
|
| Use | Pattern | When |
|
|
|-----|---------|------|
|
|
| Standard CRUD | `ModelViewSet` | Full REST API for model |
|
|
| Custom actions only | `ViewSet` | Non-standard endpoints |
|
|
| Read-only API | `ReadOnlyModelViewSet` | GET/LIST only |
|
|
| Fine control | `APIView` or `@api_view` | Custom business logic |
|
|
|
|
**ModelViewSet** (full CRUD):
|
|
|
|
```python
|
|
from rest_framework import viewsets, filters
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
|
|
class BookViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
Provides: list, create, retrieve, update, partial_update, destroy
|
|
"""
|
|
queryset = Book.objects.select_related('author').prefetch_related('tags')
|
|
serializer_class = BookSerializer
|
|
permission_classes = [IsAuthenticatedOrReadOnly]
|
|
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
|
search_fields = ['title', 'author__name']
|
|
ordering_fields = ['published_date', 'rating']
|
|
|
|
def get_queryset(self):
|
|
"""Optimize queryset based on action"""
|
|
queryset = super().get_queryset()
|
|
|
|
if self.action == 'list':
|
|
# List doesn't need full detail
|
|
return queryset.only('id', 'title', 'author__name')
|
|
|
|
return queryset
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def publish(self, request, pk=None):
|
|
"""Custom action: POST /books/123/publish/"""
|
|
book = self.get_object()
|
|
book.status = 'published'
|
|
book.published_date = timezone.now()
|
|
book.save()
|
|
return Response({'status': 'published'})
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def bestsellers(self, request):
|
|
"""Custom list action: GET /books/bestsellers/"""
|
|
books = self.get_queryset().filter(sales__gte=10000).order_by('-sales')[:10]
|
|
serializer = self.get_serializer(books, many=True)
|
|
return Response(serializer.data)
|
|
```
|
|
|
|
### Serializer Patterns
|
|
|
|
**Basic serializer with validation**:
|
|
|
|
```python
|
|
from rest_framework import serializers
|
|
from django.contrib.auth.password_validation import validate_password
|
|
|
|
class UserSerializer(serializers.ModelSerializer):
|
|
password = serializers.CharField(
|
|
write_only=True,
|
|
required=True,
|
|
validators=[validate_password]
|
|
)
|
|
password_confirm = serializers.CharField(write_only=True, required=True)
|
|
|
|
class Meta:
|
|
model = User
|
|
fields = ['id', 'username', 'email', 'password', 'password_confirm']
|
|
read_only_fields = ['id']
|
|
|
|
# Field-level validation
|
|
def validate_email(self, value):
|
|
if User.objects.filter(email__iexact=value).exists():
|
|
raise serializers.ValidationError("Email already in use")
|
|
return value.lower()
|
|
|
|
# Object-level validation (cross-field)
|
|
def validate(self, attrs):
|
|
if attrs['password'] != attrs['password_confirm']:
|
|
raise serializers.ValidationError({
|
|
'password_confirm': "Passwords don't match"
|
|
})
|
|
attrs.pop('password_confirm')
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
password = validated_data.pop('password')
|
|
user = User.objects.create(**validated_data)
|
|
user.set_password(password)
|
|
user.save()
|
|
return user
|
|
```
|
|
|
|
**Nested serializers (read-only)**:
|
|
|
|
```python
|
|
class AuthorSerializer(serializers.ModelSerializer):
|
|
book_count = serializers.IntegerField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Author
|
|
fields = ['id', 'name', 'bio', 'book_count']
|
|
|
|
class BookSerializer(serializers.ModelSerializer):
|
|
author = AuthorSerializer(read_only=True)
|
|
author_id = serializers.PrimaryKeyRelatedField(
|
|
queryset=Author.objects.all(),
|
|
source='author',
|
|
write_only=True
|
|
)
|
|
|
|
class Meta:
|
|
model = Book
|
|
fields = ['id', 'title', 'author', 'author_id', 'published_date']
|
|
```
|
|
|
|
**Dynamic fields** (include/exclude fields via query params):
|
|
|
|
```python
|
|
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
|
|
"""
|
|
Usage: /api/books/?fields=id,title,author
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
request = self.context.get('request')
|
|
if request:
|
|
fields = request.query_params.get('fields')
|
|
if fields:
|
|
fields = fields.split(',')
|
|
allowed = set(fields)
|
|
existing = set(self.fields.keys())
|
|
for field_name in existing - allowed:
|
|
self.fields.pop(field_name)
|
|
|
|
class BookSerializer(DynamicFieldsModelSerializer):
|
|
class Meta:
|
|
model = Book
|
|
fields = '__all__'
|
|
```
|
|
|
|
## Django Caching
|
|
|
|
### Cache Framework Setup
|
|
|
|
```python
|
|
# settings.py
|
|
|
|
# Redis cache (production)
|
|
CACHES = {
|
|
'default': {
|
|
'BACKEND': 'django_redis.cache.RedisCache',
|
|
'LOCATION': 'redis://127.0.0.1:6379/1',
|
|
'OPTIONS': {
|
|
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
|
'CONNECTION_POOL_KWARGS': {'max_connections': 50},
|
|
'PARSER_CLASS': 'redis.connection.HiredisParser',
|
|
},
|
|
'KEY_PREFIX': 'myapp',
|
|
'TIMEOUT': 300, # Default 5 minutes
|
|
}
|
|
}
|
|
|
|
# Memcached (alternative)
|
|
CACHES = {
|
|
'default': {
|
|
'BACKEND': 'django.core.cache.backends.memcached.PyMemcacheCache',
|
|
'LOCATION': '127.0.0.1:11211',
|
|
}
|
|
}
|
|
|
|
# Local memory (development only)
|
|
CACHES = {
|
|
'default': {
|
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
'LOCATION': 'unique-snowflake',
|
|
}
|
|
}
|
|
```
|
|
|
|
### Per-View Caching
|
|
|
|
```python
|
|
from django.views.decorators.cache import cache_page
|
|
from django.utils.decorators import method_decorator
|
|
|
|
# Function-based view
|
|
@cache_page(60 * 15) # Cache for 15 minutes
|
|
def book_list(request):
|
|
books = Book.objects.all()
|
|
return render(request, 'books/list.html', {'books': books})
|
|
|
|
# Class-based view
|
|
class BookListView(ListView):
|
|
model = Book
|
|
|
|
@method_decorator(cache_page(60 * 15))
|
|
def dispatch(self, *args, **kwargs):
|
|
return super().dispatch(*args, **kwargs)
|
|
|
|
# DRF ViewSet
|
|
from rest_framework_extensions.cache.decorators import cache_response
|
|
|
|
class BookViewSet(viewsets.ModelViewSet):
|
|
@cache_response(timeout=60*15, key_func='calculate_cache_key')
|
|
def list(self, request, *args, **kwargs):
|
|
return super().list(request, *args, **kwargs)
|
|
|
|
def calculate_cache_key(self, view_instance, view_method, request, args, kwargs):
|
|
# Custom cache key including user, filters
|
|
return f"books:list:{request.user.id}:{request.GET.urlencode()}"
|
|
```
|
|
|
|
### Low-Level Cache API
|
|
|
|
```python
|
|
from django.core.cache import cache
|
|
|
|
# Set cache
|
|
cache.set('my_key', 'my_value', timeout=300)
|
|
|
|
# Get cache
|
|
value = cache.get('my_key')
|
|
if value is None:
|
|
value = expensive_computation()
|
|
cache.set('my_key', value, timeout=300)
|
|
|
|
# Get or set (atomic)
|
|
value = cache.get_or_set('my_key', lambda: expensive_computation(), timeout=300)
|
|
|
|
# Delete cache
|
|
cache.delete('my_key')
|
|
|
|
# Clear all
|
|
cache.clear()
|
|
|
|
# Multiple keys
|
|
cache.set_many({'key1': 'value1', 'key2': 'value2'}, timeout=300)
|
|
values = cache.get_many(['key1', 'key2'])
|
|
|
|
# Increment/decrement
|
|
cache.set('counter', 0)
|
|
cache.incr('counter') # 1
|
|
cache.incr('counter', delta=5) # 6
|
|
```
|
|
|
|
### Cache Invalidation Patterns
|
|
|
|
```python
|
|
from django.db.models.signals import post_save, post_delete
|
|
from django.dispatch import receiver
|
|
|
|
@receiver([post_save, post_delete], sender=Book)
|
|
def invalidate_book_cache(sender, instance, **kwargs):
|
|
"""Invalidate cache when book changes"""
|
|
cache.delete(f'book:{instance.id}')
|
|
cache.delete('books:list') # Invalidate list cache
|
|
cache.delete(f'author:{instance.author_id}:books')
|
|
|
|
# Pattern: Cache with version tags
|
|
def get_books():
|
|
version = cache.get('books:version', 0)
|
|
cache_key = f'books:list:v{version}'
|
|
books = cache.get(cache_key)
|
|
|
|
if books is None:
|
|
books = list(Book.objects.all())
|
|
cache.set(cache_key, books, timeout=3600)
|
|
|
|
return books
|
|
|
|
def invalidate_books():
|
|
"""Bump version to invalidate all book caches"""
|
|
version = cache.get('books:version', 0)
|
|
cache.set('books:version', version + 1)
|
|
```
|
|
|
|
## Django Migrations
|
|
|
|
### Zero-Downtime Migration Pattern
|
|
|
|
**Adding NOT NULL column to large table**:
|
|
|
|
```python
|
|
# Step 1: Add nullable field (migration 0002)
|
|
class Migration(migrations.Migration):
|
|
operations = [
|
|
migrations.AddField(
|
|
model_name='user',
|
|
name='department',
|
|
field=models.CharField(max_length=100, null=True, blank=True),
|
|
),
|
|
]
|
|
|
|
# Step 2: Populate data in batches (migration 0003)
|
|
from django.db import migrations
|
|
|
|
def populate_department(apps, schema_editor):
|
|
User = apps.get_model('myapp', 'User')
|
|
|
|
# Batch update for performance
|
|
batch_size = 10000
|
|
total = User.objects.filter(department__isnull=True).count()
|
|
|
|
for offset in range(0, total, batch_size):
|
|
users = User.objects.filter(department__isnull=True)[offset:offset+batch_size]
|
|
for user in users:
|
|
user.department = determine_department(user) # Your logic
|
|
User.objects.bulk_update(users, ['department'], batch_size=batch_size)
|
|
|
|
class Migration(migrations.Migration):
|
|
dependencies = [('myapp', '0002_add_department')],
|
|
operations = [
|
|
migrations.RunPython(populate_department, migrations.RunPython.noop),
|
|
]
|
|
|
|
# Step 3: Make NOT NULL (migration 0004)
|
|
class Migration(migrations.Migration):
|
|
dependencies = [('myapp', '0003_populate_department')],
|
|
operations = [
|
|
migrations.AlterField(
|
|
model_name='user',
|
|
name='department',
|
|
field=models.CharField(max_length=100), # NOT NULL
|
|
),
|
|
]
|
|
```
|
|
|
|
### Concurrent Index Creation (PostgreSQL)
|
|
|
|
```python
|
|
from django.contrib.postgres.operations import AddIndexConcurrently
|
|
from django.db import migrations, models
|
|
|
|
class Migration(migrations.Migration):
|
|
atomic = False # Required for CONCURRENTLY operations
|
|
|
|
operations = [
|
|
AddIndexConcurrently(
|
|
model_name='book',
|
|
index=models.Index(fields=['published_date'], name='book_published_idx'),
|
|
),
|
|
]
|
|
```
|
|
|
|
### Squashing Migrations
|
|
|
|
```bash
|
|
# Squash migrations 0001 through 0020 into single migration
|
|
python manage.py squashmigrations myapp 0001 0020
|
|
|
|
# This creates migrations/0001_squashed_0020.py
|
|
# After deploying squashed migration, delete originals:
|
|
# migrations/0001.py through migrations/0020.py
|
|
```
|
|
|
|
## Django Testing
|
|
|
|
### TestCase vs TransactionTestCase
|
|
|
|
| Feature | TestCase | TransactionTestCase |
|
|
|---------|----------|---------------------|
|
|
| Speed | Fast (no DB reset between tests) | Slow (resets DB each test) |
|
|
| Transactions | Wrapped in transaction, rolled back | No automatic transaction |
|
|
| Use for | Most tests | Testing transaction behavior, signals |
|
|
|
|
**Example - TestCase**:
|
|
|
|
```python
|
|
from django.test import TestCase
|
|
from myapp.models import Book
|
|
|
|
class BookModelTest(TestCase):
|
|
@classmethod
|
|
def setUpTestData(cls):
|
|
"""Run once for entire test class (fast)"""
|
|
cls.author = Author.objects.create(name="Test Author")
|
|
|
|
def setUp(self):
|
|
"""Run before each test method"""
|
|
self.book = Book.objects.create(
|
|
title="Test Book",
|
|
author=self.author
|
|
)
|
|
|
|
def test_book_str(self):
|
|
self.assertEqual(str(self.book), "Test Book")
|
|
|
|
def test_book_author_relationship(self):
|
|
self.assertEqual(self.book.author.name, "Test Author")
|
|
```
|
|
|
|
### API Testing with DRF
|
|
|
|
```python
|
|
from rest_framework.test import APITestCase, APIClient
|
|
from rest_framework import status
|
|
from django.contrib.auth.models import User
|
|
|
|
class BookAPITest(APITestCase):
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
self.user = User.objects.create_user(
|
|
username='testuser',
|
|
password='testpass123'
|
|
)
|
|
self.book = Book.objects.create(title="Test Book")
|
|
|
|
def test_list_books_unauthenticated(self):
|
|
response = self.client.get('/api/books/')
|
|
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
|
|
|
def test_create_book_authenticated(self):
|
|
self.client.force_authenticate(user=self.user)
|
|
data = {'title': 'New Book', 'author': self.author.id}
|
|
response = self.client.post('/api/books/', data)
|
|
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
|
self.assertEqual(Book.objects.count(), 2)
|
|
|
|
def test_update_book_unauthorized(self):
|
|
other_user = User.objects.create_user(username='other', password='pass')
|
|
self.client.force_authenticate(user=other_user)
|
|
data = {'title': 'Updated Title'}
|
|
response = self.client.patch(f'/api/books/{self.book.id}/', data)
|
|
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
|
|
```
|
|
|
|
### Factory Pattern with factory_boy
|
|
|
|
```python
|
|
# tests/factories.py
|
|
import factory
|
|
from myapp.models import Author, Book
|
|
|
|
class AuthorFactory(factory.django.DjangoModelFactory):
|
|
class Meta:
|
|
model = Author
|
|
|
|
name = factory.Faker('name')
|
|
bio = factory.Faker('text', max_nb_chars=200)
|
|
|
|
class BookFactory(factory.django.DjangoModelFactory):
|
|
class Meta:
|
|
model = Book
|
|
|
|
title = factory.Faker('sentence', nb_words=4)
|
|
author = factory.SubFactory(AuthorFactory)
|
|
published_date = factory.Faker('date_this_decade')
|
|
isbn = factory.Sequence(lambda n: f'978-0-{n:09d}')
|
|
|
|
# Usage in tests
|
|
class BookTest(TestCase):
|
|
def test_book_creation(self):
|
|
book = BookFactory.create() # Creates Author too
|
|
self.assertIsNotNone(book.id)
|
|
|
|
def test_multiple_books(self):
|
|
books = BookFactory.create_batch(10) # Create 10 books
|
|
self.assertEqual(len(books), 10)
|
|
|
|
def test_author_with_books(self):
|
|
author = AuthorFactory.create()
|
|
BookFactory.create_batch(5, author=author)
|
|
self.assertEqual(author.books.count(), 5)
|
|
```
|
|
|
|
## Django Settings Organization
|
|
|
|
### Multiple Environment Configs
|
|
|
|
```
|
|
myproject/
|
|
└── settings/
|
|
├── __init__.py
|
|
├── base.py # Common settings
|
|
├── development.py # Dev overrides
|
|
├── production.py # Prod overrides
|
|
└── test.py # Test overrides
|
|
```
|
|
|
|
**settings/base.py**:
|
|
|
|
```python
|
|
import os
|
|
from pathlib import Path
|
|
|
|
BASE_DIR = Path(__file__).resolve().parent.parent.parent
|
|
|
|
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
|
|
|
|
INSTALLED_APPS = [
|
|
'django.contrib.admin',
|
|
# ...
|
|
'rest_framework',
|
|
'myapp',
|
|
]
|
|
|
|
DATABASES = {
|
|
'default': {
|
|
'ENGINE': 'django.db.backends.postgresql',
|
|
'NAME': os.environ.get('DB_NAME'),
|
|
'USER': os.environ.get('DB_USER'),
|
|
'PASSWORD': os.environ.get('DB_PASSWORD'),
|
|
'HOST': os.environ.get('DB_HOST', 'localhost'),
|
|
'PORT': os.environ.get('DB_PORT', '5432'),
|
|
}
|
|
}
|
|
```
|
|
|
|
**settings/development.py**:
|
|
|
|
```python
|
|
from .base import *
|
|
|
|
DEBUG = True
|
|
|
|
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
|
|
|
|
# Use console email backend
|
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
|
|
|
# Local cache
|
|
CACHES = {
|
|
'default': {
|
|
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
|
}
|
|
}
|
|
|
|
# Debug toolbar
|
|
INSTALLED_APPS += ['debug_toolbar']
|
|
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
|
|
INTERNAL_IPS = ['127.0.0.1']
|
|
```
|
|
|
|
**settings/production.py**:
|
|
|
|
```python
|
|
from .base import *
|
|
|
|
DEBUG = False
|
|
|
|
ALLOWED_HOSTS = [os.environ.get('ALLOWED_HOST')]
|
|
|
|
# Security settings
|
|
SECURE_SSL_REDIRECT = True
|
|
SESSION_COOKIE_SECURE = True
|
|
CSRF_COOKIE_SECURE = True
|
|
SECURE_HSTS_SECONDS = 31536000
|
|
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
|
|
SECURE_HSTS_PRELOAD = True
|
|
|
|
# Redis cache
|
|
CACHES = {
|
|
'default': {
|
|
'BACKEND': 'django_redis.cache.RedisCache',
|
|
'LOCATION': os.environ.get('REDIS_URL'),
|
|
}
|
|
}
|
|
|
|
# Real email
|
|
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
|
EMAIL_HOST = os.environ.get('EMAIL_HOST')
|
|
EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 587))
|
|
EMAIL_USE_TLS = True
|
|
```
|
|
|
|
**Usage**:
|
|
|
|
```bash
|
|
# Development
|
|
export DJANGO_SETTINGS_MODULE=myproject.settings.development
|
|
python manage.py runserver
|
|
|
|
# Production
|
|
export DJANGO_SETTINGS_MODULE=myproject.settings.production
|
|
gunicorn myproject.wsgi:application
|
|
```
|
|
|
|
## Django Deployment
|
|
|
|
### Gunicorn Configuration
|
|
|
|
```python
|
|
# gunicorn_config.py
|
|
import multiprocessing
|
|
|
|
bind = "0.0.0.0:8000"
|
|
workers = multiprocessing.cpu_count() * 2 + 1
|
|
worker_class = "sync" # or "gevent" for async
|
|
worker_connections = 1000
|
|
max_requests = 1000 # Restart workers after N requests (prevent memory leaks)
|
|
max_requests_jitter = 100
|
|
timeout = 30
|
|
keepalive = 2
|
|
|
|
# Logging
|
|
accesslog = "-" # stdout
|
|
errorlog = "-" # stderr
|
|
loglevel = "info"
|
|
|
|
# Process naming
|
|
proc_name = "myproject"
|
|
|
|
# Server mechanics
|
|
daemon = False
|
|
pidfile = "/var/run/gunicorn.pid"
|
|
```
|
|
|
|
**Systemd service**:
|
|
|
|
```ini
|
|
# /etc/systemd/system/myproject.service
|
|
[Unit]
|
|
Description=MyProject Django Application
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=notify
|
|
User=www-data
|
|
Group=www-data
|
|
WorkingDirectory=/var/www/myproject
|
|
Environment="DJANGO_SETTINGS_MODULE=myproject.settings.production"
|
|
ExecStart=/var/www/myproject/venv/bin/gunicorn \
|
|
--config /var/www/myproject/gunicorn_config.py \
|
|
myproject.wsgi:application
|
|
ExecReload=/bin/kill -s HUP $MAINPID
|
|
Restart=always
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### Static and Media Files
|
|
|
|
```python
|
|
# settings/production.py
|
|
STATIC_URL = '/static/'
|
|
STATIC_ROOT = BASE_DIR / 'staticfiles'
|
|
|
|
MEDIA_URL = '/media/'
|
|
MEDIA_ROOT = BASE_DIR / 'media'
|
|
|
|
# Use WhiteNoise for static files
|
|
MIDDLEWARE = [
|
|
'django.middleware.security.SecurityMiddleware',
|
|
'whitenoise.middleware.WhiteNoiseMiddleware', # After SecurityMiddleware
|
|
# ...
|
|
]
|
|
|
|
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
|
|
```
|
|
|
|
**Collect static files**:
|
|
|
|
```bash
|
|
python manage.py collectstatic --noinput
|
|
```
|
|
|
|
## Anti-Patterns
|
|
|
|
| Anti-Pattern | Why Bad | Fix |
|
|
|--------------|---------|-----|
|
|
| **Lazy loading in loops** | N+1 queries | Use `select_related`/`prefetch_related` |
|
|
| **No database indexing** | Slow queries | Add `db_index=True` or Meta indexes |
|
|
| **Signals for async work** | Blocks requests | Use Celery tasks instead |
|
|
| **Generic serializers for everything** | Over-fetching data | Create optimized serializers per use case |
|
|
| **No caching** | Repeated expensive queries | Cache querysets, views, template fragments |
|
|
| **Migrations in production without testing** | Downtime, data loss | Test on production-sized datasets first |
|
|
| **DEBUG=True in production** | Security risk, slow | Always DEBUG=False in production |
|
|
| **No connection pooling** | Exhausts DB connections | Use pgBouncer or django-db-geventpool |
|
|
|
|
## Cross-References
|
|
|
|
**Related skills**:
|
|
- **Database optimization** → `database-integration` (connection pooling, migrations)
|
|
- **API testing** → `api-testing` (DRF testing patterns)
|
|
- **Authentication** → `api-authentication` (DRF token auth, JWT)
|
|
- **REST API design** → `rest-api-design` (API patterns)
|
|
|
|
## Further Reading
|
|
|
|
- **Django docs**: https://docs.djangoproject.com/
|
|
- **DRF docs**: https://www.django-rest-framework.org/
|
|
- **Two Scoops of Django**: Best practices book
|
|
- **Classy Class-Based Views**: https://ccbv.co.uk/
|
|
- **Classy Django REST Framework**: https://www.cdrf.co/
|