返回博客

Django 生产级工程的分层哲学

Django 入门很快,快到容易让人误以为把业务逻辑堆进 View 就是正常的。我见过不少上线几年的系统,View 函数五百行,Model 的 save() 里塞着发邮件,ORM 查询随手就 for product in products: product.category.name——每次加需求都像在地雷阵里穿行。

问题不是写法「不优雅」,而是代码没有层次,责任没有边界,改一处就抖三处。Django 本身提供了足够的工具解决这件事,只是需要你主动搭好结构。

三层分工:Model、Service、View 各司其职

Django 的典型误区是把 Model 当「数据容器 + 业务处理器」一起用。正确的分工应该是:

  • Model:描述数据结构,提供字段约束,写 QuerySet 方法封装常用过滤链
  • Service Layer:业务逻辑唯一入口——创建订单、处理支付、触发通知都在这里
  • View / ViewSet:薄薄一层,负责解析请求、调 Service、返回响应
# 错误:业务逻辑混进 View
def purchase(request, pk):
    product = Product.objects.get(pk=pk)
    if product.stock == 0:
        return Response({'error': 'out of stock'}, status=400)
    product.stock -= 1
    product.save()
    Order.objects.create(user=request.user, product=product)
    send_confirmation_email(request.user)
    return Response({'ok': True})
 
# 正确:View 只负责 HTTP 翻译,业务在 Service
@action(detail=True, methods=['post'])
def purchase(self, request, pk=None):
    product = self.get_object()
    result = ProductService.purchase(product, request.user)
    return Response(result, status=status.HTTP_201_CREATED)

Service 函数用 @transaction.atomic 包裹涉及多表写入的操作,确保要么全部成功要么全部回滚——这是 View 层做不到也不该负责的事情。

QuerySet 方法:把「查询意图」变成可读词汇

N+1 查询是 Django 项目最常见的性能陷阱,根源是查询意图散落在各处、没有统一管理。自定义 QuerySet 可以解决这两个问题:

class ProductQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)
 
    def with_category(self):
        return self.select_related('category')
 
    def with_tags(self):
        return self.prefetch_related('tags')
 
# 调用方清晰表达意图,N+1 问题在定义处消除
Product.objects.active().with_category().with_tags()

select_related 解决外键(JOIN 一次取完),prefetch_related 解决多对多(两次查询 + Python 侧拼装)。把这两个调用封进 QuerySet 方法,调用方不需要知道底层怎么取,也不可能「忘了加」。

一个判断标准

如果一段查询逻辑在两个以上地方出现,就该封进 QuerySet 方法。如果一段业务逻辑需要操作两张以上的表,就该挪进 Service Layer。

Settings 分离:不同环境的配置不该混在一起

把所有配置塞进单文件 settings.py,然后用 if DEBUG 东拼西凑——这是很多项目的起点,也是后来维护头痛的来源。

更稳健的做法是按环境分文件:

config/settings/
├── base.py        # 所有环境共享的基础配置
├── development.py # 本地开发,开 DEBUG,装 debug_toolbar
├── production.py  # 生产,强制 HTTPS,收紧 HSTS
└── test.py        # 测试,换内存邮件后端

development.pyproduction.py 都从 base.py 继承,只覆盖差异部分。生产环境的安全配置(SECURE_SSL_REDIRECTSESSION_COOKIE_SECURESECURE_HSTS_SECONDS)就不会被开发配置的 DEBUG=True 意外冲掉。

缓存:先明确「什么数据值得缓存」

Django 提供三个层次的缓存:整页缓存(cache_page)、模板片段缓存({% cache %})、低级缓存(cache.get/set)。

高频读、低频写、计算代价高的数据适合缓存。一个典型场景是带聚合的分类列表:

def get_popular_categories():
    cache_key = 'popular_categories'
    result = cache.get(cache_key)
    if result is None:
        result = list(
            Category.objects.annotate(product_count=Count('products'))
            .filter(product_count__gt=10)
            .order_by('-product_count')[:20]
        )
        cache.set(cache_key, result, timeout=60 * 60)
    return result

注意 list() 强制执行 QuerySet——Django ORM 是惰性的,不加 list() 存进缓存的只是一个未执行的 QuerySet 对象。

一句话心法

在 Django 里,代码在哪一层写比怎么写更重要——层次清晰的项目,任何一处改动的影响范围都是可预测的。