claude-code/claude-zh/skills/django-patterns/SKILL.md

734 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: django-patterns
description: Django 架構模式、使用 DRF 的 REST API 設計、ORM 最佳實踐、快取、信號 (Signals)、中間件 (Middleware) 以及生產等級的 Django 應用程序。
---
# Django 開發模式 (Django Development Patterns)
適用於可擴充、易維護應用程序的生產等級 Django 架構模式。
## 何時啟用
- 建構 Django 網頁應用程序。
- 設計 Django REST Framework (DRF) API。
- 使用 Django ORM 和模型 (Models)。
- 設定 Django 專案結構。
- 實作快取、信號 (Signals)、中間件 (Middleware)。
## 專案結構
### 建議版面配置 (Recommended Layout)
```
myproject/
├── config/
│ ├── __init__.py
│ ├── settings/
│ │ ├── __init__.py
│ │ ├── base.py # 基礎設定
│ │ ├── development.py # 開發環境設定
│ │ ├── production.py # 生產環境設定
│ │ └── test.py # 測試環境設定
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── manage.py
└── apps/
├── __init__.py
├── users/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ ├── serializers.py
│ ├── urls.py
│ ├── permissions.py
│ ├── filters.py
│ ├── services.py
│ └── tests/
└── products/
└── ...
```
### 設定檔拆分模式 (Split Settings Pattern)
```python
# config/settings/base.py
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent.parent
SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = False
ALLOWED_HOSTS = []
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'rest_framework.authtoken',
'corsheaders',
# 在地 App
'apps.users',
'apps.products',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
WSGI_APPLICATION = 'config.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': env('DB_NAME'),
'USER': env('DB_USER'),
'PASSWORD': env('DB_PASSWORD'),
'HOST': env('DB_HOST'),
'PORT': env('DB_PORT', default='5432'),
}
}
# config/settings/development.py (開發環境)
from .base import *
DEBUG = True
ALLOWED_HOSTS = ['localhost', '127.0.0.1']
DATABASES['default']['NAME'] = 'myproject_dev'
INSTALLED_APPS += ['debug_toolbar']
MIDDLEWARE += ['debug_toolbar.middleware.DebugToolbarMiddleware']
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# config/settings/production.py (生產環境)
from .base import *
DEBUG = False
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS')
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
# 記錄日誌 (Logging)
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'file': {
'level': 'WARNING',
'class': 'logging.FileHandler',
'filename': '/var/log/django/django.log',
},
},
'loggers': {
'django': {
'handlers': ['file'],
'level': 'WARNING',
'propagate': True,
},
},
}
```
## 模型設計模式 (Model Design Patterns)
### 模型最佳實踐 (Model Best Practices)
```python
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.core.validators import MinValueValidator, MaxValueValidator
class User(AbstractUser):
"""擴展 AbstractUser 的自定義使用者模型。"""
email = models.EmailField(unique=True)
phone = models.CharField(max_length=20, blank=True)
birth_date = models.DateField(null=True, blank=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
verbose_name = 'user'
verbose_name_plural = 'users'
ordering = ['-date_joined']
def __str__(self):
return self.email
def get_full_name(self):
return f"{self.first_name} {self.last_name}".strip()
class Product(models.Model):
"""具備正確欄位配置的產品模型。"""
name = models.CharField(max_length=200)
slug = models.SlugField(unique=True, max_length=250)
description = models.TextField(blank=True)
price = models.DecimalField(
max_digits=10,
decimal_places=2,
validators=[MinValueValidator(0)]
)
stock = models.PositiveIntegerField(default=0)
is_active = models.BooleanField(default=True)
category = models.ForeignKey(
'Category',
on_delete=models.CASCADE,
related_name='products'
)
tags = models.ManyToManyField('Tag', blank=True, related_name='products')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = 'products'
ordering = ['-created_at']
indexes = [
models.Index(fields=['slug']),
models.Index(fields=['-created_at']),
models.Index(fields=['category', 'is_active']),
]
constraints = [
models.CheckConstraint(
check=models.Q(price__gte=0),
name='price_non_negative'
)
]
def __str__(self):
return self.name
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
```
### QuerySet 最佳實踐
```python
from django.db import models
class ProductQuerySet(models.QuerySet):
"""產品模型的自定義 QuerySet。"""
def active(self):
"""僅回傳啟用的產品。"""
return self.filter(is_active=True)
def with_category(self):
"""預先選取相關類別以避免 N+1 查詢問題。"""
return self.select_related('category')
def with_tags(self):
"""針對多對多關係預先抓取 (Prefetch) 標籤。"""
return self.prefetch_related('tags')
def in_stock(self):
"""回傳庫存量大於 0 的產品。"""
return self.filter(stock__gt=0)
def search(self, query):
"""依據名稱或描述搜尋產品。"""
return self.filter(
models.Q(name__icontains=query) |
models.Q(description__icontains=query)
)
class Product(models.Model):
# ... 欄位定義 ...
objects = ProductQuerySet.as_manager() # 使用自定義 QuerySet
# 使用範例
Product.objects.active().with_category().in_stock()
```
### 管理員 (Manager) 方法
```python
class ProductManager(models.Manager):
"""用於複雜查詢的自定義管理員。"""
def get_or_none(self, **kwargs):
"""回傳物件或 None而非抛出 DoesNotExist 異常。"""
try:
return self.get(**kwargs)
except self.model.DoesNotExist:
return None
def create_with_tags(self, name, price, tag_names):
"""建立產品及其關聯標籤。"""
product = self.create(name=name, price=price)
tags = [Tag.objects.get_or_create(name=name)[0] for name in tag_names]
product.tags.set(tags)
return product
def bulk_update_stock(self, product_ids, quantity):
"""批次更新多個產品的庫存。"""
return self.filter(id__in=product_ids).update(stock=quantity)
# 在模型中使用
class Product(models.Model):
# ... 欄位 ...
custom = ProductManager()
```
## Django REST Framework 模式
### 序列化程式 (Serializer) 模式
```python
from rest_framework import serializers
from django.contrib.auth.password_validation import validate_password
from .models import Product, User
class ProductSerializer(serializers.ModelSerializer):
"""產品模型的序列化程式。"""
category_name = serializers.CharField(source='category.name', read_only=True)
average_rating = serializers.FloatField(read_only=True)
discount_price = serializers.SerializerMethodField()
class Meta:
model = Product
fields = [
'id', 'name', 'slug', 'description', 'price',
'discount_price', 'stock', 'category_name',
'average_rating', 'created_at'
]
read_only_fields = ['id', 'slug', 'created_at']
def get_discount_price(self, obj):
"""計算適用折扣後的價格。"""
if hasattr(obj, 'discount') and obj.discount:
return obj.price * (1 - obj.discount.percent / 100)
return obj.price
def validate_price(self, value):
"""確保價格為非負數。"""
if value < 0:
raise serializers.ValidationError("價格不能為負數。")
return value
class ProductCreateSerializer(serializers.ModelSerializer):
"""用於建立產品的序列化程式。"""
class Meta:
model = Product
fields = ['name', 'description', 'price', 'stock', 'category']
def validate(self, data):
"""針對多個欄位的自定義驗證。"""
if data['price'] > 10000 and data['stock'] > 100:
raise serializers.ValidationError(
"高價產品不應有過多庫存。"
)
return data
class UserRegistrationSerializer(serializers.ModelSerializer):
"""使用者註冊序列化程式。"""
password = serializers.CharField(
write_only=True,
required=True,
validators=[validate_password],
style={'input_type': 'password'}
)
password_confirm = serializers.CharField(write_only=True, style={'input_type': 'password'})
class Meta:
model = User
fields = ['email', 'username', 'password', 'password_confirm']
def validate(self, data):
"""驗證兩次輸入的密碼是否一致。"""
if data['password'] != data['password_confirm']:
raise serializers.ValidationError({
"password_confirm": "密碼欄位不一致。"
})
return data
def create(self, validated_data):
"""建立帶有雜湊密碼的使用者。"""
validated_data.pop('password_confirm')
password = validated_data.pop('password')
user = User.objects.create(**validated_data)
user.set_password(password)
user.save()
return user
```
### ViewSet 模式
```python
from rest_framework import viewsets, status, filters
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, IsAdminUser
from django_filters.rest_framework import DjangoFilterBackend
from .models import Product
from .serializers import ProductSerializer, ProductCreateSerializer
from .permissions import IsOwnerOrReadOnly
from .filters import ProductFilter
from .services import ProductService
class ProductViewSet(viewsets.ModelViewSet):
"""產品模型的 ViewSet。"""
queryset = Product.objects.select_related('category').prefetch_related('tags')
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_class = ProductFilter
search_fields = ['name', 'description']
ordering_fields = ['price', 'created_at', 'name']
ordering = ['-created_at']
def get_serializer_class(self):
"""根據 Action 回傳適當的序列化程式。"""
if self.action == 'create':
return ProductCreateSerializer
return ProductSerializer
def perform_create(self, serializer):
"""保存時帶入使用者上下文。"""
serializer.save(created_by=self.request.user)
@action(detail=False, methods=['get'])
def featured(self, request):
"""回傳精選產品。"""
featured = self.queryset.filter(is_featured=True)[:10]
serializer = self.get_serializer(featured, many=True)
return Response(serializer.data)
@action(detail=True, methods=['post'])
def purchase(self, request, pk=None):
"""購買產品。"""
product = self.get_object()
service = ProductService()
result = service.purchase(product, request.user)
return Response(result, status=status.HTTP_201_CREATED)
@action(detail=False, methods=['get'], permission_classes=[IsAuthenticated])
def my_products(self, request):
"""回傳目前使用者建立的產品。"""
products = self.queryset.filter(created_by=request.user)
page = self.paginate_queryset(products)
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
```
### 自定義 Action (Custom Actions)
```python
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def add_to_cart(request):
"""將產品加入使用者購物車。"""
product_id = request.data.get('product_id')
quantity = request.data.get('quantity', 1)
try:
product = Product.objects.get(id=product_id)
except Product.DoesNotExist:
return Response(
{'error': '找不到產品'},
status=status.HTTP_404_NOT_FOUND
)
cart, _ = Cart.objects.get_or_create(user=request.user)
CartItem.objects.create(
cart=cart,
product=product,
quantity=quantity
)
return Response({'message': '已加入購物車'}, status=status.HTTP_201_CREATED)
```
## 服務層模式 (Service Layer Pattern)
```python
# apps/orders/services.py
from typing import Optional
from django.db import transaction
from .models import Order, OrderItem
class OrderService:
"""針對訂單相關業務邏輯的服務層。"""
@staticmethod
@transaction.atomic
def create_order(user, cart: Cart) -> Order:
"""從購物車建立訂單。"""
order = Order.objects.create(
user=user,
total_price=cart.total_price
)
for item in cart.items.all():
OrderItem.objects.create(
order=order,
product=item.product,
quantity=item.quantity,
price=item.product.price
)
# 清空購物車
cart.items.all().delete()
return order
@staticmethod
def process_payment(order: Order, payment_data: dict) -> bool:
"""處理訂單付款。"""
# 串接支付閘道器 (Payment Gateway)
payment = PaymentGateway.charge(
amount=order.total_price,
token=payment_data['token']
)
if payment.success:
order.status = Order.Status.PAID
order.save()
# 發送確認郵件
OrderService.send_confirmation_email(order)
return True
return False
@staticmethod
def send_confirmation_email(order: Order):
"""發送訂單確認郵件。"""
# 郵件發送邏輯
pass
```
## 快取策略 (Caching Strategies)
### 視圖層級快取 (View-Level Caching)
```python
from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
@method_decorator(cache_page(60 * 15), name='dispatch') # 快取 15 分鐘
class ProductListView(generic.ListView):
model = Product
template_name = 'products/list.html'
context_object_name = 'products'
```
### 範本片段快取 (Template Fragment Caching)
```django
{% load cache %}
{% cache 500 sidebar %}
... 高成本的側邊欄內容 ...
{% endcache %}
```
### 低階快取 (Low-Level Caching)
```python
from django.core.cache import cache
def get_featured_products():
"""獲取精選產品並執行快取。"""
cache_key = 'featured_products'
products = cache.get(cache_key)
if products is None:
products = list(Product.objects.filter(is_featured=True))
cache.set(cache_key, products, timeout=60 * 15) # 15 分鐘
return products
```
### QuerySet 快取
```python
from django.core.cache import cache
def get_popular_categories():
cache_key = 'popular_categories'
categories = cache.get(cache_key)
if categories is None:
categories = list(Category.objects.annotate(
product_count=Count('products')
).filter(product_count__gt=10).order_by('-product_count')[:20])
cache.set(cache_key, categories, timeout=60 * 60) # 1 小時
return categories
```
## 信號 (Signals)
### 信號模式 (Signal Patterns)
```python
# apps/users/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from .models import Profile
User = get_user_model()
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
"""建立使用者時同時建立 Profile。"""
if created:
Profile.objects.create(user=instance)
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
"""保存使用者時同時保存 Profile。"""
instance.profile.save()
# apps/users/apps.py
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.users'
def ready(self):
"""當 App 就緒時導入信號。"""
import apps.users.signals
```
## 中間件 (Middleware)
### 自定義中間件
```python
# middleware/active_user_middleware.py
import time
from django.utils.deprecation import MiddlewareMixin
class ActiveUserMiddleware(MiddlewareMixin):
"""用於追蹤活躍使用者的中間件。"""
def process_request(self, request):
"""處理傳入的請求。"""
if request.user.is_authenticated:
# 更新最後活動時間
request.user.last_active = timezone.now()
request.user.save(update_fields=['last_active'])
class RequestLoggingMiddleware(MiddlewareMixin):
"""用於記錄請求日誌的中間件。"""
def process_request(self, request):
"""記錄請求開始時間。"""
request.start_time = time.time()
def process_response(self, request, response):
"""記錄請求耗時。"""
if hasattr(request, 'start_time'):
duration = time.time() - request.start_time
logger.info(f'{request.method} {request.path} - {response.status_code} - {duration:.3f}s')
return response
```
## 效能優化
### 預防 N+1 查詢問題
```python
# 不良的做法 - 會產生 N+1 個查詢
products = Product.objects.all()
for product in products:
print(product.category.name) # 會針對每個產品發送獨立查詢
# 推薦的做法 - 使用 select_related 進行單一查詢
products = Product.objects.select_related('category').all()
for product in products:
print(product.category.name)
# 多對多關係推薦使用 prefetch_related
products = Product.objects.prefetch_related('tags').all()
for product in products:
for tag in product.tags.all():
print(tag.name)
```
### 資料庫索引
```python
class Product(models.Model):
name = models.CharField(max_length=200, db_index=True)
slug = models.SlugField(unique=True)
category = models.ForeignKey('Category', on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
indexes = [
models.Index(fields=['name']),
models.Index(fields=['-created_at']),
models.Index(fields=['category', 'created_at']),
]
```
### 批次操作 (Bulk Operations)
```python
# 批次建立 (Bulk create)
Product.objects.bulk_create([
Product(name=f'Product {i}', price=10.00)
for i in range(1000)
])
# 批次更新 (Bulk update)
products = Product.objects.all()[:100]
for product in products:
product.is_active = True
Product.objects.bulk_update(products, ['is_active'])
# 批次刪除
Product.objects.filter(stock=0).delete()
```
## 快速參考
| 模式 | 描述 |
|---------|-------------|
| 設定檔拆分 (Split settings) | 分離開發/生產/測試環境設定 |
| 自定義 QuerySet | 可重用的查詢方法 |
| 服務層 (Service Layer) | 業務邏輯分離 |
| ViewSet | REST API 端點 |
| 序列化與驗證 | 請求/回應轉換 |
| select_related | 外鍵 (Foreign key) 優化 |
| prefetch_related | 多對多關係優化 |
| 快取優先 (Cache first) | 先行快取高成本操作 |
| 信號 (Signals) | 事件驅動行為 |
| 中間件 (Middleware) | 請求/回應處理 |
請記住Django 提供了許多捷徑,但對於生產環境的應用程序而言,良好的結構與組織比精簡的程式碼更為重要。請為「可維護性」而建構。