Files
2026-07-13 21:35:57 +08:00

467 lines
13 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.
# Django 最佳实践
适用于 Django 5.x 和 Django REST Framework 的生产级指南。涵盖 8 个类别共 40 余条规则。
## 核心原则(7 条)
```
1. ✅ 在首次迁移前配置自定义用户模型(之后无法更改)
2. ✅ 每个领域概念对应一个 Django 应用(用户、订单、支付)
3. ✅ 胖模型、瘦视图——业务逻辑放在模型/管理器里,而不是视图里
4. ✅ 始终使用 select_related/prefetch_related(避免 N+1
5. ✅ 按环境拆分配置(base + dev + prod
6. ✅ 使用 pytest-django + factory_boy 进行测试(而不是 fixtures
7. ✅ 生产环境绝不使用 runserver(使用 Gunicorn + Nginx
```
---
## 1. 项目结构(关键)
### 按领域划分应用
```
myproject/
├── config/ # 项目配置
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py # 共享配置
│ │ ├── dev.py # DEBUG=True,可接受 SQLite
│ │ └── prod.py # DEBUG=FalsePostgresHTTPS
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/
│ ├── users/ # 自定义用户模型
│ │ ├── models.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── admin.py
│ │ ├── services.py # 业务逻辑
│ │ ├── selectors.py # 复杂查询
│ │ └── tests/
│ │ ├── test_models.py
│ │ ├── test_views.py
│ │ └── factories.py
│ ├── orders/
│ └── payments/
├── manage.py
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
└── docker-compose.yml
```
### 规则
```
✅ 一个应用 = 一个限界上下文(用户、订单、支付)
✅ 业务逻辑放在 services.py / selectors.py 中,而不是视图里
✅ 每个应用拥有自己的 urls.py、admin.py、tests/
❌ 绝不要把一切塞进同一个应用
❌ 绝不在模型层面跨应用边界导入(使用 ID)
❌ 绝不要把业务逻辑放在视图或序列化器里
```
---
## 2. 模型与迁移(关键)
### 自定义用户模型(第一天就要做!)
```python
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
# config/settings/base.py
AUTH_USER_MODEL = 'users.User'
```
**这必须在 `migrate` 之前完成。之后无法更改。**
### 模型最佳实践
```python
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Order(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='orders')
status = models.CharField(max_length=20, choices=OrderStatus.choices, default=OrderStatus.PENDING, db_index=True)
total = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
db_table = 'orders'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'status']),
]
def can_cancel(self) -> bool:
return self.status in [OrderStatus.PENDING, OrderStatus.CONFIRMED]
def cancel(self):
if not self.can_cancel():
raise ValueError(f"Cannot cancel order in {self.status} status")
self.status = OrderStatus.CANCELLED
self.save(update_fields=['status', 'updated_at'])
```
### 迁移规则
```
✅ 审查迁移 SQLpython manage.py sqlmigrate app_name 0001
✅ 为迁移起描述性名称:--name add_status_index_to_orders
✅ 将数据迁移与 schema 迁移分开
✅ 先非破坏性操作:添加列 → 回填数据 → 移除旧列
❌ 绝不编辑或删除已应用的迁移
❌ 绝不使用不带 reverse 函数的 RunPython
```
---
## 3. 视图与序列化器——DRF(高优先级)
### 服务层模式
```python
# apps/orders/services.py
from django.db import transaction
class OrderService:
@staticmethod
@transaction.atomic
def create_order(user, items_data: list[dict]) -> Order:
total = sum(item['price'] * item['quantity'] for item in items_data)
order = Order.objects.create(user=user, total=total)
OrderItem.objects.bulk_create([
OrderItem(order=order, **item) for item in items_data
])
return order
@staticmethod
def cancel_order(order_id: str, user) -> Order:
order = Order.objects.select_for_update().get(id=order_id, user=user)
order.cancel()
return order
```
### 序列化器
```python
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(many=True, read_only=True)
class Meta:
model = Order
fields = ['id', 'status', 'total', 'items', 'created_at']
read_only_fields = ['id', 'total', 'created_at']
class CreateOrderSerializer(serializers.Serializer):
"""仅用于输入的序列化器——与输出序列化器分开。"""
items = serializers.ListField(
child=serializers.DictField(), min_length=1, max_length=50,
)
def validate_items(self, items):
for item in items:
if item.get('quantity', 0) < 1:
raise serializers.ValidationError("Quantity must be at least 1")
return items
```
### 视图(保持轻薄!)
```python
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def create_order(request):
serializer = CreateOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
order = OrderService.create_order(request.user, serializer.validated_data['items'])
return Response({'data': OrderSerializer(order).data}, status=status.HTTP_201_CREATED)
```
### 规则
```
✅ 将输入序列化器与输出序列化器分开
✅ 视图只做:校验 → 调用服务 → 序列化 → 响应
✅ 多模型写入使用 @transaction.atomic
❌ 绝不把业务逻辑放在视图或序列化器里
❌ 绝不使用 ModelSerializer 进行写操作(过于隐式)
```
---
## 4. 认证(高优先级)
| 方法 | 适用场景 | 前端 |
|--------|------|----------|
| Session | 同域、SSR、Django 模板 | Django 模板 / htmx |
| JWT | 不同域、SPA、移动端 | React、Vue、移动应用 |
| OAuth2 | 第三方登录、API 消费者 | 任意 |
### JWT 配置(djangorestframework-simplejwt
```python
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
```
---
## 5. 性能优化(高优先级)
### 预防 N+1 查询
```python
# ❌ N+1:1 次查询获取订单 + N 次查询获取用户
orders = Order.objects.all()
for o in orders:
print(o.user.email) # 每次循环都访问数据库
# ✅ select_related(外键/一对一——JOIN
orders = Order.objects.select_related('user').all()
# ✅ prefetch_related(多对多/反向外键——2 次查询)
orders = Order.objects.prefetch_related('items').all()
# ✅ 组合使用
orders = Order.objects.select_related('user').prefetch_related('items').all()
```
### 查询优化工具集
```python
# 只获取需要的列
User.objects.values('id', 'email')
User.objects.values_list('email', flat=True)
# 使用注解代替 Python 循环
from django.db.models import Count, Sum
Order.objects.annotate(item_count=Count('items'), revenue=Sum('items__price'))
# 批量操作
OrderItem.objects.bulk_create([...])
Order.objects.filter(status='pending').update(status='cancelled')
# 数据库索引
class Meta:
indexes = [
models.Index(fields=['user', 'status']),
models.Index(fields=['-created_at']),
models.Index(fields=['email'], condition=Q(is_active=True)),
]
# 分页
from rest_framework.pagination import CursorPagination
class OrderPagination(CursorPagination):
page_size = 20
ordering = '-created_at'
```
### 缓存
```python
from django.core.cache import cache
def get_product(product_id: str):
cache_key = f'product:{product_id}'
product = cache.get(cache_key)
if product is None:
product = Product.objects.get(id=product_id)
cache.set(cache_key, product, timeout=300)
return product
```
---
## 6. 测试(中高优先级)
### pytest-django + factory_boy
```python
# conftest.py
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def authenticated_client(api_client, user_factory):
user = user_factory()
api_client.force_authenticate(user=user)
return api_client
```
```python
# factories.py
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
email = factory.Sequence(lambda n: f'user{n}@example.com')
username = factory.Sequence(lambda n: f'user{n}')
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'orders.Order'
user = factory.SubFactory(UserFactory)
total = factory.Faker('pydecimal', left_digits=3, right_digits=2, positive=True)
```
```python
# test_views.py
@pytest.mark.django_db
class TestListOrders:
def test_returns_user_orders(self, authenticated_client):
OrderFactory.create_batch(3, user=authenticated_client.handler._force_user)
response = authenticated_client.get('/api/orders/')
assert response.status_code == 200
assert len(response.data['data']) == 3
def test_requires_authentication(self, api_client):
response = api_client.get('/api/orders/')
assert response.status_code == 401
```
---
## 7. 管理后台定制(中等优先级)
```python
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
readonly_fields = ['price']
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'status', 'total', 'created_at']
list_filter = ['status', 'created_at']
search_fields = ['user__email', 'id']
readonly_fields = ['id', 'created_at', 'updated_at']
inlines = [OrderItemInline]
date_hierarchy = 'created_at'
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')
```
---
## 8. 生产部署(中等优先级)
### 安全配置
```python
# settings/prod.py
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
CSRF_TRUSTED_ORIGINS = ['https://example.com']
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
```
### 部署架构
```
Nginx → Gunicorn → Django
PostgreSQL + Redis (cache)
Celery (background tasks)
```
```bash
gunicorn config.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 4 \
--timeout 120 \
--access-logfile -
```
### 静态文件——WhiteNoise
```python
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # 紧跟在 SecurityMiddleware 之后
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
```
### 规则
```
✅ Gunicorn + Nginx(或 Cloud Run / Railway
✅ PostgreSQL(不要用 SQLite
✅ python manage.py check --deploy
✅ 使用 Sentry 进行错误追踪
❌ 生产环境绝不使用 runserver
❌ 生产环境绝不使用 DEBUG=True
❌ 生产环境绝不使用 SQLite
```
---
## 反模式
| # | ❌ 不要这样做 | ✅ 应该这样做 |
|---|---------|--------------|
| 1 | 在视图中写业务逻辑 | 服务层(`services.py` |
| 2 | 一个巨型应用 | 按领域划分应用 |
| 3 | 使用默认 User 模型 | 首次迁移前配置自定义 User 模型 |
| 4 | 不使用 `select_related` | 始终预加载关联对象 |
| 5 | 使用 Django fixtures 做测试 | 使用 `factory_boy` 工厂 |
| 6 | 单一 `settings.py` 文件 | 拆分:base + dev + prod |
| 7 | 生产环境使用 `runserver` | Gunicorn + Nginx |
| 8 | 生产环境使用 SQLite | PostgreSQL |
| 9 | 使用 `ModelSerializer` 进行写操作 | 显式的输入序列化器 |
| 10 | 在视图中写原生 SQL | ORM querysets + `selectors.py` |
---
## 常见问题
### 问题 1:「首次迁移后无法更改 User 模型」
**解决方案:** 如果从零开始:删除所有迁移文件 + 数据库,设置自定义 User 模型,重新迁移。如果已有数据:需进行复杂迁移(使用 `django-allauth` 或增量字段迁移)。
### 问题 2:「序列化器在处理大型查询集时速度太慢」
**解决方案:** 缺少 `select_related` / `prefetch_related` → 导致 N+1 查询。
```python
queryset = Order.objects.select_related('user').prefetch_related('items')
```
### 问题 3:「应用之间存在循环导入」
**解决方案:** 使用字符串引用:`models.ForeignKey('orders.Order', ...)` 代替导入模型类。对于服务层,在函数内部进行导入。