Django

技术栈
后端框架
pythonweb框架全栈框架ORMMTV管理后台

概览

Django 技术栈概览

Django 是一个用 Python 编写的高级全栈 Web 框架,由 Adrian Holovaty 和 Simon Willison 于 2005 年发布。它遵循"大而全"的**电池内置(Batteries Included)**哲学,提供开箱即用的 ORM、管理后台、认证系统、表单处理、模板引擎、国际化等完整组件,让开发者能专注于业务逻辑而非重复造轮子。

核心特性:

  • 🏗️ MTV 架构 — Model(数据层)、Template(表现层)、View(业务逻辑层),清晰分离
  • 🔐 安全内置 — 自动防护 XSS、CSRF、SQL 注入、点击劫持等常见攻击
  • 🗄️ 强大 ORM — 支持 PostgreSQL、MySQL、SQLite、Oracle 等多数据库,迁移系统成熟
  • 🎛️ 自动管理后台 — 基于模型定义自动生成 CRUD 后台界面,几乎零代码
  • 🔄 中间件系统 — 请求/响应处理管道,可插拔扩展
  • 📍 URL 路由 — 正则/路径表达式路由,反向解析
  • 🌍 国际化/本地化 — 内置翻译框架和多语言支持
  • 🚀 高流量就绪 — Instagram、Pinterest、Disqus 等亿级用户站点均使用 Django

适用场景: 内容管理系统(CMS)、电商平台、社交网络、数据管理后台、企业级 Web 应用、API 服务(DRF)。

安装

1. 环境准备

  • 操作系统: Windows 10+ / macOS 11+ / Linux(Ubuntu 20.04+ 推荐)
  • Python 版本: Python 3.10 及以上(Django 5.0+ 要求 3.10+)
  • 数据库: SQLite(开发默认)/ PostgreSQL 15+ / MySQL 8.0+(生产推荐)
  • 依赖项: pip、virtualenv/venv

创建并激活虚拟环境

python -m venv venv
# Windows:
venv\Scripts\activate
# macOS / Linux:
source venv/bin/activate

2. 安装命令

# 安装最新版 Django
pip install django

# 安装指定版本
pip install "django>=5.0,<5.1"

# 验证安装
django-admin --version
# 输出: 5.0.x

# 安装常用依赖
pip install psycopg2-binary   # PostgreSQL 驱动
pip install mysqlclient        # MySQL 驱动
pip install django-debug-toolbar  # 调试工具
pip install django-cors-headers   # 跨域支持
pip install djangorestframework   # DRF REST API 框架
pip install celery[redis]          # 异步任务

# 创建 Django 项目
django-admin startproject mysite
cd mysite

# 运行开发服务器
python manage.py runserver
# 浏览器访问 http://127.0.0.1:8000/

3. 常用安装问题

问题 1:django-admin 命令找不到

# 确认虚拟环境已激活(终端有 (venv) 前缀)
# 如果仍找不到,直接用 Python 运行
python -m django startproject mysite

问题 2:pip 安装慢 / 超时

pip install django -i https://pypi.tuna.tsinghua.edu.cn/simple
# 或永久配置
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

问题 3:mysqlclient 安装失败(Linux)

# Debian/Ubuntu 需先安装系统依赖
sudo apt-get install python3-dev default-libmysqlclient-dev build-essential
# 再安装
pip install mysqlclient

问题 4:macOS psycopg2 安装失败

# 用 Homebrew 安装 PostgreSQL
brew install postgresql
# 再安装
pip install psycopg2-binary

问题 5:端口 8000 被占用

# 指定其他端口启动
python manage.py runserver 8080
# 或绑定所有 IP
python manage.py runserver 0.0.0.0:8000

问题 6:数据库迁移报"关系不存在"

# 确保迁移已应用
python manage.py makemigrations
python manage.py migrate
# 如果仍有问题,重建迁移
python manage.py migrate --fake app_name zero
python manage.py migrate app_name

示例

Django Hello World —— 你的第一个 Django 项目

目标

创建 Django 项目与第一个应用,理解:

  • Django 项目 vs 应用的区别
  • urls.py 路由与 views.py 视图的关系
  • 模板渲染与动态数据传递
  • manage.py 命令行工具

完整步骤

步骤 1:创建项目

django-admin startproject mysite
cd mysite

生成的结构:

mysite/
├── manage.py           # Django 命令行工具
└── mysite/
    ├── __init__.py
    ├── settings.py     # 全局配置
    ├── urls.py         # 根 URL 路由
    ├── asgi.py         # ASGI 入口
    └── wsgi.py         # WSGI 入口

步骤 2:创建应用

python manage.py startapp hello
hello/
├── __init__.py
├── admin.py        # 管理后台注册
├── apps.py         # 应用配置
├── models.py       # 数据库模型
├── views.py        # 视图函数/类
├── urls.py         # 应用路由(需手动创建)
├── tests.py        # 测试
└── migrations/     # 数据库迁移

步骤 3:编写视图

# hello/views.py
from django.shortcuts import render
from django.http import HttpResponse, JsonResponse
from django.views import View
from datetime import datetime


# 方式一:函数视图(最简单)
def index(request):
    """首页 —— 返回纯文本"""
    return HttpResponse("""
        <h1>🚀 Hello, Django!</h1>
        <ul>
            <li><a href='/hello/'>/hello/</a> — 问候</li>
            <li><a href='/hello/?name=Vibe'>/hello/?name=Vibe</a> — 带参数</li>
            <li><a href='/api/status/'>/api/status/</a> — JSON 接口</li>
        </ul>
    """)


def hello(request):
    """查询参数问候"""
    name = request.GET.get("name", "World")
    return HttpResponse(f"Hello, {name}!")


def status(request):
    """JSON 响应"""
    return JsonResponse({
        "status": "ok",
        "framework": "Django",
        "version": "5.0",
        "server_time": datetime.now().isoformat(),
    })


# 方式二:类视图(适合 RESTful)
class HelloView(View):
    def get(self, request):
        name = request.GET.get("name", "World")
        return JsonResponse({"message": f"Hello, {name}!", "method": "GET"})

    def post(self, request):
        import json
        try:
            data = json.loads(request.body)
            name = data.get("name", "World")
        except json.JSONDecodeError:
            name = "World"
        return JsonResponse({"message": f"Hello, {name}!", "method": "POST"}, status=201)

步骤 4:配置路由

# hello/urls.py(新建此文件)
from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("hello/", views.hello, name="hello"),
    path("api/status/", views.status, name="status"),
    # 类视图用 as_view()
    path("api/hello/", views.HelloView.as_view(), name="hello_api"),
]
# mysite/urls.py(修改根路由,include 应用路由)
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("hello.urls")),  # 引入应用路由
]

步骤 5:注册应用

# mysite/settings.py
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "hello",  # ← 添加此行
]

步骤 6:运行

python manage.py runserver

# 访问以下 URL:
# http://127.0.0.1:8000/
# http://127.0.0.1:8000/hello/?name=Vibe
# http://127.0.0.1:8000/api/status/
# http://127.0.0.1:8000/api/hello/
# curl -X POST http://127.0.0.1:8000/api/hello/ -H "Content-Type: application/json" -d '{"name":"Django"}'

预期输出

GET /api/status/ 返回:

{
  "status": "ok",
  "framework": "Django",
  "version": "5.0",
  "server_time": "2025-01-15T10:30:00.123456"
}

POST /api/hello/ 返回:

{"message": "Hello, Django!", "method": "POST"}

Django 请求处理流程

浏览器请求
    ↓
URL Dispatcher(urls.py)—— 匹配路由
    ↓
View(views.py) —— 处理业务逻辑
    ↓
Model(models.py)—— 操作数据库(可选)
    ↓
Template(templates/)—— 渲染 HTML(可选)
    ↓
HttpResponse —— 返回给浏览器

关键要点

概念 说明
项目 vs 应用 项目 = 整个网站;应用 = 一个功能模块(如博客、用户)
urlpatterns URL 路由列表,从上到下匹配,先匹配先服务
path(str, view, name) 路由定义:URL模式 → 视图 → 命名(用于反向解析)
include() 将子路由表插入到当前路由,实现模块化
request.GET 查询参数字典(QueryDict)
JsonResponse 自动 JSON 序列化 + Content-Type: application/json

Django REST Framework — 构建生产级 RESTful API

目标

使用 Django REST Framework (DRF) 构建完整的 RESTful API,实现对"文章"资源的 CRUD:

  • 理解 Model → Serializer → ViewSet → Router 的 DRF 管线
  • 掌握序列化器(Serializer)的验证与输出控制
  • 掌握 ViewSet 与 GenericAPIView 的使用
  • 实现分页、过滤、搜索、排序

完整代码

步骤 1:安装 DRF

pip install djangorestframework django-filter

步骤 2:配置

# mysite/settings.py
INSTALLED_APPS = [
    # ... Django 默认应用
    "rest_framework",
    "django_filters",
    "blog",  # 我们的应用
]

REST_FRAMEWORK = {
    "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
    "PAGE_SIZE": 10,
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ],
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework.authentication.SessionAuthentication",
        "rest_framework.authentication.TokenAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticatedOrReadOnly",
    ],
}

步骤 3:定义模型

# blog/models.py
from django.db import models
from django.contrib.auth.models import User


class Category(models.Model):
    """文章分类"""
    name = models.CharField("分类名", max_length=100, unique=True)
    slug = models.SlugField(max_length=100, unique=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        verbose_name_plural = "categories"

    def __str__(self):
        return self.name


class Post(models.Model):
    """博客文章"""
    STATUS_CHOICES = [
        ("draft", "草稿"),
        ("published", "已发布"),
        ("archived", "已归档"),
    ]

    title = models.CharField("标题", max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    content = models.TextField("内容")
    status = models.CharField("状态", max_length=20, choices=STATUS_CHOICES, default="draft")
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name="posts")
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="posts")
    views_count = models.IntegerField("浏览量", default=0)
    created_at = models.DateTimeField("创建时间", auto_now_add=True)
    updated_at = models.DateTimeField("更新时间", auto_now=True)

    class Meta:
        ordering = ["-created_at"]

    def __str__(self):
        return self.title

步骤 4:编写序列化器

# blog/serializers.py
from rest_framework import serializers
from django.contrib.auth.models import User
from .models import Post, Category


class CategorySerializer(serializers.ModelSerializer):
    post_count = serializers.SerializerMethodField()

    class Meta:
        model = Category
        fields = ["id", "name", "slug", "post_count", "created_at"]
        read_only_fields = ["id", "created_at"]

    def get_post_count(self, obj):
        return obj.posts.count()


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ["id", "username", "email"]


class PostListSerializer(serializers.ModelSerializer):
    """列表用 —— 只返回摘要,不包含全文"""
    author = UserSerializer(read_only=True)
    category_name = serializers.CharField(source="category.name", read_only=True)

    class Meta:
        model = Post
        fields = ["id", "title", "slug", "status", "author", "category_name",
                  "views_count", "created_at"]
        read_only_fields = ["id", "author", "views_count", "created_at"]


class PostDetailSerializer(serializers.ModelSerializer):
    """详情用 —— 返回完整内容"""
    author = UserSerializer(read_only=True)
    category = CategorySerializer(read_only=True)
    category_id = serializers.PrimaryKeyRelatedField(
        queryset=Category.objects.all(), source="category", write_only=True
    )

    class Meta:
        model = Post
        fields = "__all__"
        read_only_fields = ["id", "author", "views_count", "slug", "created_at", "updated_at"]

    def validate_title(self, value):
        """自定义字段级验证"""
        if len(value) < 3:
            raise serializers.ValidationError("标题至少需要3个字符")
        return value

步骤 5:编写 ViewSet

# blog/views.py
from rest_framework import viewsets, permissions, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post, Category
from .serializers import (
    PostListSerializer, PostDetailSerializer, CategorySerializer
)


class PostViewSet(viewsets.ModelViewSet):
    """文章 CRUD ViewSet"""
    queryset = Post.objects.select_related("author", "category").all()
    filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
    filterset_fields = ["status", "category__slug"]
    search_fields = ["title", "content"]
    ordering_fields = ["created_at", "views_count", "title"]

    def get_serializer_class(self):
        if self.action == "list":
            return PostListSerializer
        return PostDetailSerializer

    def get_permissions(self):
        if self.action in ["create", "update", "partial_update", "destroy"]:
            return [permissions.IsAuthenticated()]
        return [permissions.AllowAny()]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

    # 自定义 action
    @action(detail=True, methods=["post"])
    def increment_views(self, request, pk=None):
        """自定义动作:增加浏览量"""
        post = self.get_object()
        post.views_count += 1
        post.save(update_fields=["views_count"])
        return Response({"views_count": post.views_count})

    @action(detail=False, methods=["get"])
    def my_posts(self, request):
        """获取当前用户的文章"""
        posts = self.queryset.filter(author=request.user)
        serializer = self.get_serializer(posts, many=True)
        return Response(serializer.data)


class CategoryViewSet(viewsets.ReadOnlyModelViewSet):
    """分类 —— 只读"""
    queryset = Category.objects.all()
    serializer_class = CategorySerializer
    lookup_field = "slug"  # 用 slug 而非 id 查找

步骤 6:配置路由

# blog/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from . import views

router = DefaultRouter()
router.register(r"posts", views.PostViewSet, basename="post")
router.register(r"categories", views.CategoryViewSet, basename="category")

urlpatterns = [
    path("api/", include(router.urls)),
]
# mysite/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("", include("blog.urls")),
]

步骤 7:迁移与运行

python manage.py makemigrations blog
python manage.py migrate
python manage.py createsuperuser  # 创建管理用户
python manage.py runserver

API 端点一览

方法 URL 说明
GET /api/posts/ 文章列表(分页+过滤+搜索)
POST /api/posts/ 创建文章(需认证)
GET /api/posts/{id}/ 文章详情
PUT /api/posts/{id}/ 全量更新(需认证)
PATCH /api/posts/{id}/ 部分更新(需认证)
DELETE /api/posts/{id}/ 删除(需认证)
POST /api/posts/{id}/increment_views/ 自增浏览量
GET /api/posts/my_posts/ 我的文章(需认证)
GET /api/categories/ 分类列表
GET /api/categories/{slug}/ 分类详情

测试命令

# 获取文章列表(分页)
curl http://127.0.0.1:8000/api/posts/

# 过滤 + 搜索
curl "http://127.0.0.1:8000/api/posts/?status=published&search=Django&ordering=-views_count"

# 创建文章
curl -X POST http://127.0.0.1:8000/api/posts/ \
  -H "Content-Type: application/json" \
  -u admin:password \
  -d '{"title":"Hello DRF","content":"# 我的第一篇文章","status":"published","category_id":1}'

关键要点

概念 说明
ModelSerializer 根据模型自动生成序列化字段,减少样板代码
ModelViewSet 一次性提供 list/create/retrieve/update/destroy 全套
@action 在 ViewSet 上添加自定义端点
select_related 预加载外键关联,避免 N+1 查询
lookup_field 将默认的 pk 查找改为其他字段(如 slug)
DefaultRouter 自动按 REST 惯例生成 URL 路由

教程

Django 入门教程 —— MTV 架构与快速建站

本章目标

  • 理解 Django 的 MTV 架构与"电池内置"哲学
  • 掌握模型、视图、模板、路由四大核心
  • 完成一个完整的博客系统 CRUD

1. Django 的 MTV 架构

Django 采用 MTV(Model-Template-View)分层:

          URL Dispatcher
               │
               ▼
    ┌──────────────────────┐
    │   View(业务逻辑)     │  ← "Controller" 的角色
    │   处理请求、协调 Model  │
    │   和 Template           │
    └────┬─────────┬────────┘
         │         │
         ▼         ▼
  ┌──────────┐  ┌──────────┐
  │  Model   │  │ Template │
  │ 数据层   │  │ 表现层   │
  │ ORM + DB │  │ HTML/模板│
  └──────────┘  └──────────┘

与传统 MVC 的对应关系:

  • Model ≈ M
  • Template ≈ V
  • View ≈ C(Controller)

2. 模型(Model)—— 数据层

# blog/models.py
from django.db import models
from django.utils.text import slugify


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)
    slug = models.SlugField(max_length=50, unique=True, blank=True)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.name)
        super().save(*args, **kwargs)

    def __str__(self):
        return self.name


class Post(models.Model):
    title = models.CharField("标题", max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    body = models.TextField("正文")
    tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ["-created_at"]
        indexes = [
            models.Index(fields=["-created_at"]),
            models.Index(fields=["slug"]),
        ]

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

    def get_absolute_url(self):
        from django.urls import reverse
        return reverse("post_detail", kwargs={"slug": self.slug})

模型字段类型速查

字段 用途
CharField(max_length=n) 短文本
TextField() 长文本
IntegerField() 整数
BooleanField() 布尔值
DateTimeField(auto_now_add=True) 创建时间
DateTimeField(auto_now=True) 更新时间
ForeignKey(to, on_delete) 一对多
ManyToManyField(to) 多对多
SlugField() URL 友好字符串
EmailField() 邮箱
ImageField(upload_to=...) 图片上传

查询 API(ORM)

# 基本 CRUD
Post.objects.create(title="Hello", body="World")          # 创建
post = Post.objects.get(slug="hello")                      # 单个查询
posts = Post.objects.filter(tags__name="django")           # 过滤
Post.objects.filter(status="draft").update(status="pub")   # 批量更新
Post.objects.filter(created_at__year=2024).delete()        # 批量删除

# 聚合
from django.db.models import Count, Avg, Sum
Post.objects.aggregate(Count("id"))                        # 总数

# 关联查询
post.tags.all()                                            # 获取文章的所有标签
tag.posts.filter(status="published")                       # 获取标签下已发布文章

# 链式条件
Post.objects.filter(
    models.Q(status="published") & 
    (models.Q(title__icontains="Django") | models.Q(body__icontains="Django"))
)

3. 视图(View)—— 业务逻辑

函数视图 vs 类视图

# 函数视图 —— 简单直接
def post_list(request):
    posts = Post.objects.filter(status="published")
    return render(request, "blog/list.html", {"posts": posts})

# 类视图 —— 可复用、带 mixin
from django.views.generic import ListView, DetailView, CreateView

class PostListView(ListView):
    model = Post
    template_name = "blog/list.html"
    context_object_name = "posts"
    paginate_by = 10

    def get_queryset(self):
        queryset = super().get_queryset().filter(status="published")
        tag_slug = self.kwargs.get("tag_slug")
        if tag_slug:
            queryset = queryset.filter(tags__slug=tag_slug)
        return queryset

常用通用视图

类视图 用途 关键属性
ListView 对象列表 model, paginate_by, queryset
DetailView 对象详情 model, slug_field, slug_url_kwarg
CreateView 创建对象 model, fields, success_url
UpdateView 更新对象 同上
DeleteView 删除对象 model, success_url
TemplateView 纯模板 template_name

4. 模板(Template)—— 表现层

<!-- templates/blog/list.html -->
{% extends "base.html" %}

{% block title %}博客文章列表{% endblock %}

{% block content %}
<h1>📝 文章列表</h1>

{% for post in posts %}
    <article>
        <h2><a href="{{ post.get_absolute_url }}">{{ post.title }}</a></h2>
        <p>{{ post.body|truncatewords:30 }}</p>
        <div class="meta">
            <time>{{ post.created_at|date:"Y-m-d" }}</time>
            <span class="tags">
                {% for tag in post.tags.all %}
                    <a href="{% url 'post_list_by_tag' tag.slug %}">{{ tag.name }}</a>
                {% endfor %}
            </span>
        </div>
    </article>
{% empty %}
    <p>暂无文章</p>
{% endfor %}

<!-- 分页 -->
{% if page_obj.has_other_pages %}
    <nav class="pagination">
        {% if page_obj.has_previous %}
            <a href="?page={{ page_obj.previous_page_number }}">上一页</a>
        {% endif %}
        <span>第 {{ page_obj.number }} / {{ page_obj.paginator.num_pages }} 页</span>
        {% if page_obj.has_next %}
            <a href="?page={{ page_obj.next_page_number }}">下一页</a>
        {% endif %}
    </nav>
{% endif %}
{% endblock %}

模板标签速查

标签 说明
{% extends %} 继承父模板
{% block %} 定义可替换的内容块
{% include %} 引入子模板
{% url "name" %} 反向 URL 解析
{% csrf_token %} CSRF 令牌
{% for %} / {% endfor %} 循环
{% if %} / {% endif %} 条件
{% with %} 变量赋值
{% now "Y-m-d" %} 当前日期时间

模板过滤器

过滤器 效果
`{{ text truncatewords:30 }}`
`{{ date date:"Y-m-d" }}`
`{{ content safe }}`
`{{ text linebreaks }}`
`{{ value default:"暂无" }}`

5. 管理后台自定义

# blog/admin.py
from django.contrib import admin
from .models import Post, Tag


@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
    list_display = ["name", "slug"]
    prepopulated_fields = {"slug": ("name",)}


@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
    list_display = ["title", "slug", "created_at", "updated_at"]
    list_filter = ["tags", "created_at"]
    search_fields = ["title", "body"]
    prepopulated_fields = {"slug": ("title",)}
    date_hierarchy = "created_at"
    filter_horizontal = ["tags"]
    readonly_fields = ["created_at", "updated_at"]

    fieldsets = [
        ("基本信息", {"fields": ["title", "slug", "body"]}),
        ("分类", {"fields": ["tags"]}),
        ("时间信息", {"fields": ["created_at", "updated_at"]}),
    ]

6. 常用管理命令

python manage.py runserver              # 开发服务器
python manage.py startapp <;app_name>    # 创建应用
python manage.py makemigrations         # 生成迁移文件
python manage.py migrate                # 执行迁移
python manage.py createsuperuser        # 创建管理员
python manage.py shell                  # Django 交互式 Shell
python manage.py collectstatic          # 收集静态文件
python manage.py test                   # 运行测试
python manage.py showmigrations         # 查看迁移状态
python manage.py dbshell                # 数据库命令行

思考题

  1. Django 的 MTV 与经典 MVC 有何异同?为什么 Django 称其 View 为 Controller?
  2. ForeignKeyon_delete=models.CASCADE vs SET_NULL 有何区别?
  3. 什么场景应该使用函数视图而非类视图?
  4. Django 管理后台适合直接给最终用户使用吗?为什么?

Django 进阶实战 —— 测试、安全与部署

本章目标

  • 编写单元测试与集成测试
  • 理解 Django 安全机制与最佳实践
  • 使用 PostgreSQL + Gunicorn + Nginx 生产部署
  • 掌握 Django 性能优化技巧

1. 测试体系

1.1 单元测试

# blog/tests/test_models.py
from django.test import TestCase
from django.contrib.auth.models import User
from blog.models import Post, Tag


class PostModelTest(TestCase):
    def setUp(self):
        """每个测试方法前运行"""
        self.user = User.objects.create_user(username="testuser", password="testpass")
        self.tag = Tag.objects.create(name="Django")
        self.post = Post.objects.create(
            title="Test Post",
            body="This is a test post content.",
        )
        self.post.tags.add(self.tag)

    def test_post_creation(self):
        """测试文章创建"""
        self.assertEqual(self.post.title, "Test Post")
        self.assertEqual(self.post.slug, "test-post")
        self.assertIsNotNone(self.post.created_at)

    def test_post_str(self):
        """测试 __str__ 方法"""
        self.assertEqual(str(self.post), "Test Post")

    def test_tag_relationship(self):
        """测试多对多关联"""
        self.assertEqual(self.post.tags.count(), 1)
        self.assertEqual(self.post.tags.first().name, "Django")
        self.assertIn(self.post, self.tag.posts.all())

1.2 视图测试

# blog/tests/test_views.py
from django.test import TestCase
from django.urls import reverse


class PostViewTest(TestCase):
    def setUp(self):
        self.user = User.objects.create_user(username="admin", password="admin123")
        self.post = Post.objects.create(
            title="Published Post", body="Content", status="published"
        )

    def test_post_list_view(self):
        """测试列表页返回 200 且包含文章"""
        url = reverse("post_list")
        response = self.client.get(url)
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "Published Post")
        self.assertTemplateUsed(response, "blog/list.html")

    def test_post_detail_404(self):
        """测试不存在的文章返回 404"""
        url = reverse("post_detail", kwargs={"slug": "no-such-post"})
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_create_post_requires_login(self):
        """测试未登录用户重定向到登录页"""
        url = reverse("post_create")
        response = self.client.get(url)
        self.assertRedirects(response, f"/accounts/login/?next={url}")

1.3 API 测试(DRF)

from rest_framework.test import APITestCase
from rest_framework import status


class PostAPITest(APITestCase):
    def test_list_posts(self):
        response = self.client.get("/api/posts/")
        self.assertEqual(response.status_code, status.HTTP_200_OK)

    def test_create_unauthenticated(self):
        response = self.client.post("/api/posts/", {"title": "x"}, format="json")
        self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)

运行测试

# 运行所有测试
python manage.py test

# 运行特定应用的测试
python manage.py test blog

# 运行特定测试类
python manage.py test blog.tests.test_models.PostModelTest

# 带覆盖率(需安装 coverage)
pip install coverage
coverage run manage.py test
coverage report
coverage html  # 生成 HTML 报告

2. 安全最佳实践

2.1 内置安全防护

Django 默认提供以下保护,无需额外配置:

防护项 机制
XSS 跨站脚本 模板自动 HTML 转义
CSRF 跨站请求伪造 CSRF 中间件 + {% csrf_token %}
SQL 注入 ORM 参数化查询
点击劫持 X-Frame-Options: DENY
内容嗅探 X-Content-Type-Options: nosniff

2.2 生产环境配置

# settings_production.py
import os

DEBUG = False  # 必须关闭!

# 秘密密钥从环境变量读取,不要硬编码
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

# HTTPS 安全 Cookie
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

# HSTS(强制 HTTPS)
SECURE_HSTS_SECONDS = 31536000  # 一年
SECURE_HSTS_INCLUDE_SUBDOMAINS = True
SECURE_HSTS_PRELOAD = True

# 只允许指定 Host
ALLOWED_HOSTS = ["example.com", "www.example.com"]

# 密码哈希——使用最强算法
PASSWORD_HASHERS = [
    "django.contrib.auth.hashers.Argon2PasswordHasher",
    "django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
]

# 文件上传大小限制
DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024  # 10MB

2.3 常见安全问题

问题:开发中把 SECRET_KEY 提交到版本库

# .gitignore
.env
*.local
settings_local.py

问题:SQL 注入风险(避免 raw SQL)

# ❌ 危险!
Post.objects.raw(f"SELECT * FROM blog_post WHERE title = '{user_input}'")

# ✅ 安全——使用 ORM
Post.objects.filter(title=user_input)

3. 生产部署方案

架构

用户 → Nginx(反向代理 + 静态文件 + SSL)→ Gunicorn(WSGI 服务器)→ Django

3.1 PostgreSQL 配置

# settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["DB_NAME"],
        "USER": os.environ["DB_USER"],
        "PASSWORD": os.environ["DB_PASSWORD"],
        "HOST": os.environ.get("DB_HOST", "localhost"),
        "PORT": os.environ.get("DB_PORT", "5432"),
        "CONN_MAX_AGE": 600,  # 持久连接
        "OPTIONS": {
            "connect_timeout": 5,
        },
    }
}

3.2 Gunicorn 启动

gunicorn mysite.wsgi:application \
  -w 4 \                              # worker 进程数
  -b unix:/run/gunicorn.sock \        # Unix socket(比 TCP 更快)
  --access-logfile /var/log/gunicorn/access.log \
  --error-logfile /var/log/gunicorn/error.log \
  --log-level info

3.3 Nginx 配置

server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # 安全头
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;

    # 静态文件 —— Nginx 直接服务
    location /static/ {
        alias /var/www/mysite/static/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    # 媒体文件
    location /media/ {
        alias /var/www/mysite/media/;
    }

    # 动态请求代理到 Gunicorn
    location / {
        proxy_pass http://unix:/run/gunicorn.sock;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

4. 性能优化清单

4.1 数据库优化

# 使用 select_related(一对一/外键)和 prefetch_related(多对多)
# ❌ N+1 查询
posts = Post.objects.all()
# ✅ 一次 JOIN
posts = Post.objects.select_related("author").prefetch_related("tags")

# 只取需要的字段
Post.objects.values("id", "title", "created_at")

# 大数据量分批处理
Post.objects.filter(...).iterator(chunk_size=1000)

4.2 缓存策略

# 视图缓存
from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # 15分钟
def post_list(request):
    ...

# 模板片段缓存
{% load cache %}
{% cache 300 "sidebar" %}
    {# 昂贵的渲染逻辑 #}
{% endcache %}

# 低级缓存 API
from django.core.cache import cache
cache.set("hot_posts", posts, timeout=300)
posts = cache.get("hot_posts")

4.3 中间件精简

# 只保留必需的中间件,移除 CSRF(仅纯 API)、Message 等
MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",  # 纯 API 可移除
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",              # 纯 API 可移除
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
]

思考题

  1. select_related()prefetch_related() 的区别是什么?
  2. Django 的 CSRF 保护原理是——攻击者如何构造 CSRF 攻击?Django 如何防护?
  3. 如何安全地存储 Django SECRET_KEY?有哪些反模式需避免?
  4. 纯 API 服务的 Django 可以移除哪些中间件和模板相关配置?

参考资料

  1. [1] William S. Vincent. Django for APIs: Build web APIs with Python & Django. 2024.
  2. [2] Daniel Roy Greenfeld, Audrey Roy Greenfeld. Two Scoops of Django 3.x. 2020.
  3. [3] Django Software Foundation. Django 官方文档. 2024.
  4. [4] Encode OSS. Django REST Framework 文档. 2024.