一、入门基础

1. 环境安装与项目创建

推荐使用 Python 3.10+,先装好 Django 再创建项目

# 安装 Django
pip install django

# 创建项目(末尾的 . 表示在当前目录创建,避免多一层嵌套)
django-admin startproject myproject .

# 创建应用
python manage.py startapp myapp

项目创建后会在根目录生成 manage.py,这是日常开发最常用的命令入口

应用创建后需在 settings.pyINSTALLED_APPS 中注册才能生效

2. 项目结构

myproject/
├── manage.py          # 命令行工具入口
├── myproject/
│   ├── settings.py    # 项目配置文件
│   ├── urls.py        # 根路由
│   ├── wsgi.py        # 部署入口(同步)
│   └── asgi.py        # 部署入口(异步)
└── myapp/
    ├── models.py      # 数据模型
    ├── views.py       # 视图
    ├── urls.py        # 应用路由(需手动创建)
    ├── admin.py       # 后台配置
    └── migrations/    # 迁移文件

3. 常用命令

先记住这5个,日常开发基本够用

python manage.py runserver              # 启动开发服务器(默认127.0.0.1:8000)
python manage.py makemigrations         # 检测model变化,生成迁移文件
python manage.py migrate                # 执行迁移,应用到数据库
python manage.py createsuperuser       # 创建后台管理员
python manage.py shell                 # 进入Django交互环境

makemigrationsmigrate 是一对,先 makemigrations 再 migrate

runserver 支持热重载,改代码自动重启

其他常用命令

python manage.py startapp myapp         # 创建应用
python manage.py test myapp             # 运行测试
python manage.py collectstatic          # 收集静态文件(部署用)
python manage.py dbshell                # 进入数据库命令行
python manage.py showmigrations         # 查看迁移状态
python manage.py dumpdata myapp > data.json  # 导出数据
python manage.py loaddata data.json     # 导入数据
python manage.py check --deploy         # 部署前检查

4. 自定义管理命令

app/management/commands/ 下创建文件(需 __init__.py),每个文件就是一个命令

# app/management/commands/greet.py
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    help = "向用户打招呼"

    def add_arguments(self, parser):
        parser.add_argument("name", type=str, help="用户名")
        parser.add_argument("--count", type=int, default=1, help="重复次数")
        parser.add_argument("--uppercase", action="store_true", help="大写输出")

    def handle(self, *args, **options):
        name = options["name"]
        for _ in range(options["count"]):
            msg = f"Hello, {name}!"
            if options["uppercase"]:
                msg = msg.upper()
            self.stdout.write(self.style.SUCCESS(msg))
  • add_arguments 定义命令行参数,位置参数不带 --,可选参数带
  • action="store_true" 表示布尔标志,出现为 True
  • self.style.SUCCESS 绿色输出,还有 ERROR(红)、WARNING(黄)

用法:python manage.py greet Alice --count 3 --uppercase


二、Settings 配置

1. 数据库配置

DATABASES 是字典,default 是默认连接

DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",  # mysql / sqlite3 / oracle
        "NAME": "mydb",
        "USER": "myuser",
        "PASSWORD": "mypassword",
        "HOST": "127.0.0.1",     # 空字符串=localhost
        "PORT": "5432",          # 空字符串=默认端口
        "CONN_MAX_AGE": 60,      # 连接复用秒数,0=每次请求新建连接
    }
}

SQLite 最简单,开发常用:
"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "db.sqlite3"

CONN_MAX_AGE 设非零值可复用连接,适合高并发

2. 模板配置

DIRS 是全局模板目录,APP_DIRS=True 让 Django 也去各应用 templates/ 下找

TEMPLATES = [{
    "BACKEND": "django.template.backends.django.DjangoTemplates",
    "DIRS": [BASE_DIR / "templates"],
    "APP_DIRS": True,
    "OPTIONS": {
        "context_processors": [
            "django.contrib.auth.context_processors.auth",      # 注入 user
            "django.contrib.messages.context_processors.messages",  # 注入消息
        ],
    },
}]
  • context_processors 列表里的函数会在每次渲染时自动注入全局变量

3. 静态文件与媒体

STATIC_URL = "/static/"                      # URL前缀
STATICFILES_DIRS = [BASE_DIR / "static"]     # 开发时源目录
STATIC_ROOT = BASE_DIR / "staticfiles"       # collectstatic收集目录(部署用)

MEDIA_URL = "/media/"                        # 上传文件的URL前缀
MEDIA_ROOT = BASE_DIR / "media"              # 上传文件存储路径

STATIC_ROOT 只有部署时 collectstatic 才用到,开发环境不用管

4. 缓存配置

CACHES = {
    "default": {
        "BACKEND": "django.core.cache.backends.redis.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "TIMEOUT": 300,          # 默认过期秒数,None=永不过期
        "KEY_PREFIX": "myapp",   # key前缀,多项目共享Redis时隔离
    }
}

开发环境可以用本地内存缓存,不用装 Redis:
"BACKEND": "django.core.cache.backends.locmem.LocMemCache"

5. 常用配置速查

配置项 说明 默认值
DEBUG 调试模式,生产必须 False False
ALLOWED_HOSTS 允许的Host头 []
LANGUAGE_CODE 语言 “en-us”
TIME_ZONE 时区 “UTC”
USE_TZ 时区感知 True
DEFAULT_AUTO_FIELD 默认主键类型 “BigAutoField”
LOGIN_URL 未登录重定向地址 “/accounts/login/”
SESSION_COOKIE_AGE Session过期秒数 1209600(2周)

三、Model 模型

1. 模型与字段

Model 是 ORM 的核心,每个类对应一张表,属性对应列

from django.db import models

class Product(models.Model):
    name = models.CharField(max_length=200)
    price = models.DecimalField(max_digits=10, decimal_places=2)
    stock = models.IntegerField(default=0)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
  • auto_now_add=True 创建时自动设为当前时间,之后不变
  • auto_now=True 每次 save() 时自动更新
  • 两个字段都不能在表单中手动编辑

基本字段类型

字段 说明 必填参数
CharField 短文本 max_length
TextField 长文本
IntegerField 整数
FloatField 浮点数
DecimalField 精确小数 max_digits, decimal_places
BooleanField 布尔
UUIDField UUID default=uuid.uuid4
JSONField JSON数据
DateField 日期
DateTimeField 日期时间
EmailField 邮箱(自动验证格式)
URLField URL(自动验证格式)
SlugField URL友好文本
FileField 文件上传 upload_to
ImageField 图片上传(需Pillow) upload_to

ImageFieldFileField 的区别:Image 会验证是否为有效图片,File 可以上传任何文件

upload_to 的两种写法

# 方式1:字符串,支持日期格式化
document = models.FileField(upload_to="docs/%Y/%m/%d/")

# 方式2:函数,根据对象动态生成路径
def user_path(instance, filename):
    return f"users/{instance.user.id}/{filename}"
avatar = models.ImageField(upload_to=user_path)

2. 字段选项

所有字段共享的通用参数

选项 作用层 说明
null 数据库 是否允许 NULL
blank 表单 是否允许为空
default 两者 默认值
choices 表单 枚举选项
unique 数据库 唯一约束
db_index 数据库 创建索引
primary_key 数据库 设为主键
validators 表单 验证器列表
help_text 表单 帮助文本
verbose_name 两者 人类可读名
error_messages 表单 自定义错误消息

null 和 blank 的区别(新手最容易搞混)

null=True 是数据库层面的,允许该列存 NULL

blank=True 是表单层面的,允许表单提交时空着

可选昵称字段:nick = models.CharField(max_length=50, null=True, blank=True)

choices 枚举

Django 3.0+ 推荐用 TextChoices,类型安全

class Post(models.Model):
    class Status(models.TextChoices):
        DRAFT = "draft", "草稿"
        PUBLISHED = "published", "已发布"
        ARCHIVED = "archived", "已归档"

    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.DRAFT,
    )

# 使用
Post.objects.filter(status=Post.Status.PUBLISHED)
post.get_status_display()    # 返回 "已发布"
  • 第一个值是存储到数据库的值,第二个是显示文本
  • get_<field>_display() 返回中文标签

3. 关系字段

ForeignKey(多对一,最常用)

class Post(models.Model):
    category = models.ForeignKey(
        Category,
        on_delete=models.CASCADE,
        related_name="posts",          # 反向查询名: category.posts
        related_query_name="post",    # 反向过滤名
        null=True,
        blank=True,
        limit_choices_to={"is_active": True},
    )
  • related_name 定义反向查询名,不设默认用小写模型名
  • limit_choices_to 限制 Admin/表单中可选的选项

on_delete 选项

效果 适用场景
CASCADE 删父→子也删 子记录完全依赖父记录(如明细行)
PROTECT 有子数据则禁止删父 业务主数据,防止误删
SET_NULL 删父→子字段置NULL 子记录独立存在(需 null=True
SET_DEFAULT 删父→设为默认值 有合理默认值时
RESTRICT 类似PROTECT,但允许通过CASCADE路径删 比PROTECT更灵活

CASCADE 和 RESTRICT 的区别:PROTECT 无论如何都不让删;RESTRICT 如果父对象本身也是被 CASCADE 删除的,就允许删

ManyToManyField(多对多)

┌──────────────┐          ┌──────────────┐
│   Post       │          │   Tag        │
└──────────────┘          └──────────────┘
       │                          │
       │    关系保存在中间表中       │
       ▼                          ▼
┌──────────────────────────────────────────┐
│         中间表 PostTag                    │
├──────────────────────────────────────────┤
│  id  |  post_id    |  tag_id             │
└──────────────────────────────────────────┘
class Post(models.Model):
    tags = models.ManyToManyField(
        "Tag",
        related_name="posts",
        through="PostTag",      # 自定义中间表(可选)
        blank=True,
    )
  • Many2many 不会在 Post 或 Tag 表中存储关系,关系存在中间表
  • through 指定自定义中间表,可以在中间表加额外字段(如添加时间)

4. Meta 内部类

Meta 配置元数据,不生成列,但影响表结构和查询行为

class Meta:
    ordering = ["-published_at", "title"]   # 默认排序
    db_table = "blog_posts"                  # 自定义表名
    verbose_name = "文章"
    verbose_name_plural = "文章列表"

约束与索引

class Meta:
    constraints = [
        # title 和 author 组合唯一:同一作者不能发同名文章
        models.UniqueConstraint(
            fields=["title", "author"],
            name="unique_title_per_author",
        ),
        # 条件唯一约束:每个作者只能有一篇已发布
        models.UniqueConstraint(
            fields=["author"],
            condition=models.Q(published_at__isnull=False),
            name="unique_published_per_author",
        ),
    ]
    indexes = [
        models.Index(fields=["-published_at"], name="pub_date_idx"),
    ]

其他常用 Meta 选项

选项 说明
abstract=True 抽象基类,不创建表,子类继承字段
proxy=True 代理模型,不创建表,只覆盖方法
permissions 添加自定义权限 [("code", "描述")]
get_latest_by latest()/earliest() 的默认排序字段
managed Django是否管理迁移,False则手动管理表结构

5. Model 方法

str 和 save

def __str__(self):
    return f"{self.name} (¥{self.price})"

def save(self, *args, **kwargs):
    if self.stock < 0:        # 保存前修正
        self.stock = 0
    super().save(*args, **kwargs)  # 调用父类完成保存
    # 保存后可以做日志、通知等
  • save(update_fields=["name"]) 只更新指定字段,性能更好

clean 验证

clean() 是模型级验证,可以跨字段联合检查

from django.core.exceptions import ValidationError

def clean(self):
    if self.price < 0:
        raise ValidationError({"price": "价格不能为负"})
    if self.discount_price and self.discount_price >= self.price:
        raise ValidationError("折扣价必须低于原价")

@property 和 get_absolute_url

@property
def is_in_stock(self):
    return self.stock > 0

def get_absolute_url(self):
    from django.urls import reverse
    return reverse("product_detail", kwargs={"pk": self.pk})
  • get_absolute_url Admin 中"查看站点"按钮会用到

F 表达式更新(避免竞态条件)

并发场景下 self.stock += 1; self.save() 有竞态问题——两个请求可能读到相同的旧值

from django.db.models import F

# ✅ 正确:让数据库做原子更新 stock = stock + 1
Product.objects.filter(pk=1).update(stock=F("stock") + 1)

字段间比较也要用 F:Product.objects.filter(stock__lt=F("min_stock"))


四、ORM 查询

1. QuerySet 基础

QuerySet 是惰性的——创建时不会查数据库,迭代/切片/计数时才真正执行SQL

可以链式调用,最后统一优化成一条SQL

# 返回 QuerySet(可链式调用)
Product.objects.all()
Product.objects.filter(price__gt=100)
Product.objects.exclude(stock=0)

# 返回单个对象
Product.objects.get(pk=1)               # 不存在抛 DoesNotExist
Product.objects.first()                  # 无数据返回 None
Product.objects.latest("created_at")     # 按字段取最新

创建和更新

# 创建
Product.objects.create(name="iPhone", price=999)

# get_or_create: 查不到就创建(原子操作)
obj, created = Product.objects.get_or_create(
    name="iPad", defaults={"price": 799}
)
# created=True 表示新建,False 表示已存在

# 批量更新(不触发save和信号)
Product.objects.filter(stock=0).update(is_active=False)

# 批量创建
Product.objects.bulk_create([
    Product(name="AirPods", price=199),
    Product(name="Magic", price=99),
])

update() 直接执行 SQL UPDATE,不会触发 save() 和信号

需要触发信号时要逐个 save()

排序、去重、限制

Product.objects.order_by("-price")              # 降序
Product.objects.order_by("-price", "name")     # 多字段排序
Product.objects.values("category").distinct()  # 去重
Product.objects.all()[:10]                      # 前10条 (LIMIT 10)
Product.objects.all()[5:15]                     # 偏移5取10条

不支持负索引,[-1] 会报错

2. 字段选择与性能

values / values_list

只需要部分字段时用,返回字典/元组,不实例化对象,性能更好

# values: 返回字典列表
Product.objects.values("name", "price")
# [{'name': 'iPhone', 'price': 999}, ...]

# values_list: 返回元组列表
Product.objects.values_list("name", "price")
# [('iPhone', 999), ...]

# flat=True: 单字段时返回扁平列表
Product.objects.values_list("name", flat=True)
# ['iPhone', 'iPad', ...]

解决 N+1 查询问题的两个方法

N+1 问题:
查询10篇文章 → 1条SQL
访问每篇文章的作者 → 10条SQL(每次访问都查一次)
共11条SQL,性能很差

解决:
select_related:  JOIN 一次查出(适合外键、一对一)
prefetch_related: 两次查询后Python拼接(适合多对多、反向关系)
# select_related: 外键 JOIN,单次查询
Product.objects.select_related("category").all()
# 之后访问 product.category 不会再查数据库

# prefetch_related: 多对多,两次查询
Post.objects.prefetch_related("tags").all()

# 深层预取
Post.objects.select_related("author__profile").prefetch_related("tags")

# 带过滤的预取
from django.db.models import Prefetch
Post.objects.prefetch_related(
    Prefetch("comments", queryset=Comment.objects.filter(is_approved=True))
)

3. 聚合与注解

aggregate 对全部数据聚合,返回字典

annotate 为每条记录添加计算字段

from django.db.models import Count, Sum, Avg

# aggregate: 返回一个字典
Product.objects.aggregate(
    total=Count("id"),
    avg_price=Avg("price"),
    total_stock=Sum("stock"),
)
# {'total': 100, 'avg_price': 500, 'total_stock': 5000}

# annotate: 每条记录加一个字段
Category.objects.annotate(
    product_count=Count("product"),       # 每个分类有多少商品
    total_stock=Sum("product__stock"),
)
# 之后每个 category 对象可访问 .product_count

条件表达式 Case/When

from django.db.models import Case, When, Value, Q

# 条件计数:每个分类有多少高价商品
Category.objects.annotate(
    expensive_count=Count("product", filter=Q(product__price__gt=500)),
)

# CASE WHEN: 按价格分级
Product.objects.annotate(
    price_level=Case(
        When(price__lt=100, then=Value("low")),
        When(price__lt=500, then=Value("medium")),
        When(price__gte=500, then=Value("high")),
        default=Value("unknown"),
    ),
)

4. Field Lookups 查询类型

语法:字段名__lookup类型=值

Lookup 说明 示例
exact 精确匹配(默认) name="iPhone"
iexact 不区分大小写 name__iexact="iphone"
contains 包含子串 name__contains="Phone"
icontains 包含(不区分大小写) name__icontains="phone"
startswith 以…开头 name__startswith="i"
gt / gte 大于 / 大于等于 price__gt=100
lt / lte 小于 / 小于等于 price__lt=100
in 在列表中 id__in=[1, 2, 3]
isnull 是否为NULL pub_date__isnull=True
range 闭区间范围 price__range=(100, 500)
regex 正则匹配 name__regex=r"^\d+"

日期查询

Post.objects.filter(pub_date__year=2024)       # 按年
Post.objects.filter(pub_date__month=7)         # 按月 (1-12)
Post.objects.filter(pub_date__day=15)          # 按日
Post.objects.filter(pub_date__week=28)         # ISO周数
Post.objects.filter(pub_date__week_day=1)      # 星期 (1=周日)
Post.objects.filter(pub_date__quarter=3)       # 季度 (1-4)

日期 lookup 可以叠加:pub_date__year__gte=2023

5. Q 对象 — 复杂条件组合

filter() 默认用 AND 连接。需要 OR、NOT 时用 Q 对象

from django.db.models import Q

# OR: 价格低于50 或 价格高于500
Product.objects.filter(Q(price__lt=50) | Q(price__gt=500))

# NOT: 库存不为0
Product.objects.filter(~Q(stock=0))

# 混合
Product.objects.filter(
    Q(name__icontains="phone") & (Q(price__lt=100) | Q(stock__gt=50))
)
  • & = AND,| = OR,~ = NOT

动态构建 Q(搜索功能常用)

queries = Q()
if search_term:
    queries |= Q(name__icontains=search_term)
    queries |= Q(description__icontains=search_term)
if category:
    queries &= Q(category__name=category)
Product.objects.filter(queries)

6. F 对象 — 引用字段值

让比较和更新在数据库层面完成,最常见的用途是原子更新

from django.db.models import F

# ❌ 有竞态条件:先读后写,并发时可能读到旧值
# product.stock += 1; product.save()

# ✅ 原子更新:数据库直接执行 stock = stock + 1
Product.objects.filter(pk=1).update(stock=F("stock") + 1)

# 字段间比较
Product.objects.filter(stock__lt=F("min_stock_level"))

# 批量涨价10%
Product.objects.update(price=F("price") * 1.1)

五、视图 Views

1. 函数视图 FBV

最基础的视图形式:接收 HttpRequest,返回 HttpResponse

from django.shortcuts import render, get_object_or_404, redirect

def product_detail(request, pk):
    product = get_object_or_404(Product, pk=pk)

    if request.method == "POST":
        name = request.POST.get("name", "")
        return redirect("product_list")

    return render(request, "products/detail.html", {"product": product})

request 常用属性

属性 说明
request.method “GET” / “POST”
request.GET GET参数(QueryDict)
request.POST POST表单数据
request.FILES 上传文件
request.user 当前用户
request.session Session对象
request.path 路径(不含域名)
request.headers 请求头(不区分大小写)

request.POST.get("key", "默认值") 获取参数

request.POST.getlist("tags") 获取多值字段(如checkbox)

返回 JSON

from django.http import JsonResponse

def api_product(request, pk):
    product = get_object_or_404(Product, pk=pk)
    return JsonResponse({"id": product.id, "name": product.name})
  • JsonResponse(data, safe=False) 返回列表等非字典时需 safe=False

2. 类视图 CBV

CBV 用类组织代码,通过继承减少重复。Django 内置了通用视图封装常见 CRUD

ListView 列表页

from django.views.generic import ListView

class ProductListView(ListView):
    model = Product
    template_name = "products/list.html"
    context_object_name = "products"    # 模板中变量名
    paginate_by = 10                    # 自动分页

    def get_queryset(self):
        # 重写查询集,添加过滤
        qs = super().get_queryset()
        return qs.filter(is_active=True)

DetailView / CreateView / UpdateView / DeleteView

from django.views.generic import DetailView, CreateView, DeleteView
from django.urls import reverse_lazy

class ProductDetailView(DetailView):
    model = Product
    template_name = "products/detail.html"
    context_object_name = "product"

class ProductCreateView(CreateView):
    model = Product
    fields = ["name", "price", "stock", "category"]
    success_url = reverse_lazy("product_list")

    def form_valid(self, form):
        form.instance.owner = self.request.user  # 保存前注入数据
        return super().form_valid(form)

class ProductDeleteView(DeleteView):
    model = Product
    success_url = reverse_lazy("product_list")
  • fields 指定表单包含哪些字段,"__all__" 表示全部
  • form_valid() 在表单验证通过后调用,可以注入额外数据

Mixin 权限控制

Mixin 必须放在继承列表最左边

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

class EditorView(LoginRequiredMixin, PermissionRequiredMixin, View):
    login_url = "/accounts/login/"
    permission_required = "app.can_edit"
    raise_exception = True   # True=无权限返回403,False=重定向登录

CBV 方法执行流程

请求进入 → dispatch() → 根据method分发
  GET  → get() → get_queryset() → get_context_data() → 渲染模板
  POST → post() → get_form() → form_valid()/form_invalid() → save + redirect
方法 调用时机 常见重写用途
dispatch() 入口 权限检查
get_queryset() 获取查询集 添加过滤
get_object() 获取单对象 自定义查询
get_context_data() 构建上下文 添加模板变量
form_valid(form) 表单通过 保存前注入数据

六、URL 路由

1. 基本路由

from django.urls import path, include
from . import views

urlpatterns = [
    path("", views.home, name="home"),
    path("products/", views.product_list, name="product_list"),
    path("products/<int:pk>/", views.product_detail, name="product_detail"),
    path("articles/<slug:slug>/", views.article_detail, name="article_detail"),

    # include: 将 blog/ 下的路由委托给 blog 应用
    path("blog/", include("blog.urls")),
]
  • <int:pk> 从 URL 中提取参数,int 是转换器,pk 是参数名
  • name 给路由起名,代码中用 reverse() 反向解析

2. URL 转换器

转换器 匹配 Python类型
str 非空字符串,不含 / str
int 正整数 int
slug 字母数字加 - _ str
uuid UUID格式 UUID
path 任何字符串,含 / str

自定义转换器

class FourDigitYearConverter:
    regex = r"[0-9]{4}"

    def to_python(self, value):
        return int(value)

    def to_url(self, value):
        return f"{value:04d}"

register_converter(FourDigitYearConverter, "yyyy")
# 使用: path("archive/<yyyy:year>/", views.archive)
  • regex 匹配正则,to_python 转为Python对象,to_url 转回URL字符串

3. reverse 反向解析

不要在代码中硬编码 URL,用 reverse() 根据 name 解析

from django.urls import reverse

url = reverse("product_detail", kwargs={"pk": 1})
# 返回 "/products/1/"

模板中用 {% url 'product_detail' pk=1 %}


七、模板 Templates

1. 变量与过滤器

{{ }} 输出变量,{% %} 执行标签,| 应用过滤器

{{ name|default:"匿名" }}              {# 为空时显示默认值 #}
{{ price|floatformat:2 }}              {# 保留2位小数 #}
{{ content|truncatewords:30 }}          {# 截取前30个单词 #}
{{ content|truncatechars:100 }}         {# 截取前100个字符 #}
{{ text|striptags }}                   {# 去除HTML标签 #}
{{ text|safe }}                        {# 标记安全,不转义HTML #}
{{ list|join:", " }}                    {# 用逗号连接列表 #}
{{ value|length }}                     {# 长度 #}
{{ value|upper }}                      {# 转大写 #}
{{ value|date:"Y-m-d H:i" }}           {# 日期格式化 #}
{{ value|filesizeformat }}             {# 文件大小格式化 #}
{{ value|yesno:"是,否,未知" }}         {# True,False,None映射 #}

2. 控制流

{% if user.is_authenticated %}
    欢迎,{{ user.username }}
{% elif user.is_anonymous %}
    请登录
{% endif %}

{% for product in products %}
    {{ forloop.counter }}       {# 从1开始的序号 #}
    {{ forloop.first }}         {# 是否第一条 #}
    {{ forloop.last }}          {# 是否最后一条 #}
    {{ product.name }}
{% empty %}
    没有数据
{% endfor %}

运算符:== != < > in not in and or not

3. 模板继承

{# base.html #}
<html>
<body>
    {% block content %}{% endblock %}
</body>
</html>

{# child.html #}
{% extends "base.html" %}
{% block content %}
    {{ block.super }}  {# 保留父模板内容 #}
    子页面内容
{% endblock %}
  • 父模板用 {% block %} 定义可替换区域,子模板用同名 block 覆盖

4. 其他常用标签

{% csrf_token %}               {# 表单中必须包含 #}
{% url 'product_detail' pk=1 %} {# 反向解析URL #}
{% load static %}
<img src="{% static 'images/logo.png' %}">
{% include "partials/_nav.html" %}  {# 包含其他模板 #}
{% now "Y-m-d" %}               {# 输出当前时间 #}
{% with total=products|length %}  {# 定义局部变量 #}
    共 {{ total }} 件
{% endwith %}

八、表单 Forms

1. Form

Form 处理 HTML渲染 + 数据验证 + 数据清洗

from django import forms

class ContactForm(forms.Form):
    name = forms.CharField(
        max_length=100,
        label="姓名",
        widget=forms.TextInput(attrs={"class": "form-control"}),
        help_text="请输入真实姓名",
        error_messages={"required": "姓名不能为空"},
    )
    email = forms.EmailField(label="邮箱")    # 自动验证邮箱格式
    message = forms.CharField(widget=forms.Textarea(attrs={"rows": 4}))
    subscribe = forms.BooleanField(required=False)
  • widget 控制 HTML 渲染方式,attrs 添加 HTML 属性
  • BooleanField 默认 required=True,意味着必须勾选

验证

字段级 clean_<field>() + 表单级 clean()

# 字段级验证
def clean_name(self):
    name = self.cleaned_data["name"]
    if "spam" in name.lower():
        raise forms.ValidationError("名称包含敏感词")
    return name  # 必须返回清洗后的值

# 表单级验证(可访问多个字段)
def clean(self):
    cleaned_data = super().clean()
    email = cleaned_data.get("email")
    subscribe = cleaned_data.get("subscribe")
    if subscribe and not email:
        self.add_error("email", "订阅需要填写邮箱")
    return cleaned_data
  • clean_<field>() 只关注单个字段
  • clean() 可以跨字段联合检查
  • add_error() 不会中断后续验证

2. ModelForm

从 Model 自动生成表单,避免重复定义

class ProductForm(forms.ModelForm):
    class Meta:
        model = Product
        fields = ["name", "price", "stock", "category"]
        labels = {"name": "产品名称", "price": "价格"}
        widgets = {
            "name": forms.TextInput(attrs={"class": "form-control"}),
            "description": forms.Textarea(attrs={"rows": 4}),
        }

    def save(self, commit=True):
        instance = super().save(commit=False)
        # 保存前可注入额外数据
        if commit:
            instance.save()
            self.save_m2m()   # 保存多对多关系
        return instance
  • fields = "__all__" 包含所有字段,exclude 排除指定字段
  • commit=False 返回未保存的实例,可进一步修改

3. 字段与 Widget 对应

Form 字段 默认 Widget 说明
CharField TextInput 文本输入
EmailField EmailInput 邮箱(自动验证)
IntegerField NumberInput 整数
BooleanField CheckboxInput 复选框
ChoiceField Select 下拉选择
DateField DateInput 日期
FileField ClearableFileInput 文件上传
ImageField ClearableFileInput 图片上传

九、Admin 后台

1. 列表页配置

from django.contrib import admin

@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
    list_display = ("name", "price", "stock", "is_active")
    list_filter = ("is_active", "category")        # 右侧过滤器
    search_fields = ("name", "description")         # 搜索框
    list_editable = ("price", "stock")              # 列表页直接编辑
    date_hierarchy = "created_at"                   # 日期导航
    list_per_page = 25                              # 每页条数
  • list_editable 中的字段不能同时在 list_display_links

2. 编辑页配置

fieldsets = (
    (None, {"fields": ("name", "category")}),
    ("价格与库存", {
        "fields": ("price", "stock"),
        "classes": ("collapse",),   # 可折叠
    }),
)
readonly_fields = ("created_at",)
prepopulated_fields = {"slug": ("name",)}  # slug自动从name生成
autocomplete_fields = ("category",)        # 外键搜索框
  • prepopulated_fields 前端 JS 自动填充,适合 slug
  • autocomplete_fields 需要目标模型配置了 search_fields

3. 自定义列与批量操作

from django.utils.html import format_html

@admin.display(description="状态", ordering="is_active")
def colored_status(self, obj):
    color = "green" if obj.is_active else "red"
    text = "上架" if obj.is_active else "下架"
    return format_html('<span style="color:{};">{}</span>', color, text)

@admin.action(description="标记为下架")
def make_inactive(self, request, queryset):
    updated = queryset.update(is_active=False)
    self.message_user(request, f"{updated} 个产品已下架")
  • @admin.displayordering 指定点击列头排序时用的字段
  • @admin.action 出现在列表页"动作"下拉菜单

4. Inline 内联编辑

在父模型编辑页中同时编辑子模型

class OrderItemInline(admin.TabularInline):   # 或 StackedInline
    model = OrderItem
    extra = 3           # 初始空行数

@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
    inlines = [OrderItemInline]
  • TabularInline 表格排列,StackedInline 垂直堆叠

十、认证系统 Auth

1. 登录登出

from django.contrib.auth import authenticate, login, logout

def login_view(request):
    user = authenticate(request, username=username, password=password)
    if user is not None:
        login(request, user)        # 写入session完成登录
        return redirect("home")

def logout_view(request):
    logout(request)
    return redirect("login")

authenticate() 只验证凭据,不登录。必须再调 login() 才算完成登录

2. 用户管理

from django.contrib.auth import get_user_model
User = get_user_model()

# 创建用户(密码自动加密)
user = User.objects.create_user(username="alice", password="secret123")

# 修改密码
user.set_password("newpassword")
user.save()  # set_password后必须save

# 验证密码
user.check_password("oldpassword")  # 返回 bool

千万不要明文存储密码,create_user / set_password 会自动哈希

3. 权限与分组

# 检查权限
request.user.has_perm("app.can_edit")
request.user.has_perms(["app.can_edit", "app.can_delete"])

# 分组管理
from django.contrib.auth.models import Group
group = Group.objects.get(name="Editors")
group.user_set.add(user)

Django 自动为每个 Model 生成四种权限:add change delete view

格式:appname.动作_modelname

4. 装饰器

from django.contrib.auth.decorators import login_required, permission_required

@login_required(login_url="/login/")
def dashboard(request): ...

@permission_required("app.can_edit", raise_exception=True)
def edit(request): ...
  • raise_exception=True 无权限返回403,False则重定向登录页

5. 自定义 User 模型

强烈建议在项目初始就设置,中途切换非常困难

# models.py
from django.contrib.auth.models import AbstractUser

class CustomUser(AbstractUser):
    phone = models.CharField(max_length=20, blank=True)
    avatar = models.ImageField(upload_to="avatars/", blank=True)

# settings.py
AUTH_USER_MODEL = "accounts.CustomUser"
  • 继承 AbstractUser 保留所有内置功能,只需添加额外字段
  • 必须在第一次 migrate 之前设置 AUTH_USER_MODEL

十一、中间件 Middleware

1. 工厂函数式

中间件是洋葱模型:请求从上到下,响应从下到上

def simple_middleware(get_response):
    # 一次性初始化(项目启动时)

    def middleware(request):
        # 请求到达视图前
        request.custom_attr = "value"
        response = get_response(request)  # 调用下一层
        # 视图返回响应后
        response["X-Custom-Header"] = "Hello"
        return response

    return middleware

2. 类式中间件

class TimingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        import time
        start = time.time()
        response = self.get_response(request)
        response["X-Response-Time"] = f"{time.time()-start:.3f}s"
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        # 视图执行前
        # 返回None继续,返回HttpResponse短路
        return None

    def process_exception(self, request, exception):
        # 视图抛异常时
        # 返回None异常继续传播,返回HttpResponse已处理
        return None

注册在 MIDDLEWARE 列表中,顺序很重要

请求按列表从上到下执行,响应按逆序从下到上


十二、信号 Signals

1. 内置信号

当某些事件发生时(如对象保存),发送通知给接收器

信号 触发时机
pre_save / post_save save() 前/后
pre_delete / post_delete delete() 前/后
m2m_changed 多对多关系变化
user_logged_in / user_logged_out 用户登录/登出

2. 接收信号

from django.db.models.signals import post_save
from django.dispatch import receiver

@receiver(post_save, sender=Product)
def product_created(sender, instance, created, **kwargs):
    # sender: 模型类
    # instance: 保存的对象
    # created: True=新建,False=更新
    if created:
        print(f"新产品创建: {instance.name}")
  • sender 限定只接收特定模型的信号

信号通常放在 AppConfig.ready() 中注册

3. 自定义信号

from django.dispatch import Signal, receiver

# 定义信号
order_completed = Signal()

# 发送
order_completed.send(sender=Order, order=order, user=user)

# 接收
@receiver(order_completed)
def send_email(sender, **kwargs):
    order = kwargs["order"]
    # 发邮件...
  • send() 同步调用所有接收器
  • send_robust() 会捕获接收器抛出的异常

十三、常用装饰器

装饰器 说明
@require_http_methods(["GET","POST"]) 限制HTTP方法
@require_GET / @require_POST 仅允许GET/POST
@login_required 要求登录
@permission_required("perm") 检查权限
@user_passes_test(func) 自定义条件
@csrf_exempt 豁免CSRF检查
@cache_page(60) 缓存视图60秒
@never_cache 禁止缓存
@vary_on_headers("User-Agent") 按请求头区分缓存
@gzip_page Gzip压缩
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from django.views.decorators.cache import cache_page

@require_POST
@login_required(login_url="/login/")
def create(request): ...

@cache_page(60 * 15)
def slow_view(request): ...

十四、分页 Paginator

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

product_list = Product.objects.all()
paginator = Paginator(product_list, 10)  # 每页10条

page_number = request.GET.get("page", 1)
try:
    page_obj = paginator.page(page_number)
except PageNotAnInteger:
    page_obj = paginator.page(1)       # 不是数字返回第一页
except EmptyPage:
    page_obj = paginator.page(paginator.num_pages)  # 超范围返回最后一页

Page 对象属性

属性/方法 说明
page_obj.object_list 当前页数据
page_obj.number 当前页码
page_obj.has_next() 是否有下一页
page_obj.has_previous() 是否有上一页
page_obj.next_page_number() 下一页码

orphans 参数:最后一页少于该数量时合并到上一页,避免出现只有一两条的末页


十五、缓存 Cache

1. 低级缓存 API

from django.core.cache import cache

cache.set("key", "value", timeout=300)          # 缓存5分钟
value = cache.get("key", "默认值")                # 不存在返回默认值
cache.get_or_set("key", lambda: expensive(), 300)  # 不存在则设置
cache.delete("key")
cache.clear()                                    # 清空所有
cache.incr("counter")                            # 自增

get_or_set 是原子操作,避免"先查后设"的竞态问题

2. 视图级缓存

from django.views.decorators.cache import cache_page

@cache_page(60 * 15)  # 缓存15分钟
def slow_view(request):
    # 第一次请求执行,后续直接返回缓存
    ...

3. 模板片段缓存

{% load cache %}
{% cache 500 sidebar request.user.username %}
    {# 按username区分缓存 #}
    {% for item in sidebar_items %}
        {{ item.name }}
    {% endfor %}
{% endcache %}

十六、测试 Testing

1. TestCase 类型

数据库 适用场景
SimpleTestCase 不涉及数据库
TestCase 涉及数据库(最常用)
TransactionTestCase 测试事务行为

2. 模型测试

from django.test import TestCase

class ProductModelTest(TestCase):
    @classmethod
    def setUpTestData(cls):
        # 类级别初始化,只执行一次
        cls.product = Product.objects.create(name="iPhone", price=999)

    def test_str(self):
        self.assertEqual(str(self.product), "iPhone (¥999)")

3. 视图测试

from django.test import Client
from django.urls import reverse

class ViewTest(TestCase):
    def setUp(self):
        self.client = Client()

    def test_list_view(self):
        response = self.client.get(reverse("product_list"))
        self.assertEqual(response.status_code, 200)
        self.assertTemplateUsed(response, "products/list.html")

    def test_login_required(self):
        response = self.client.get(reverse("dashboard"))
        self.assertRedirects(response, "/accounts/login/?next=/dashboard/")

    def test_authenticated(self):
        self.client.force_login(self.user)  # 跳过认证直接登录
        response = self.client.get(reverse("dashboard"))
        self.assertEqual(response.status_code, 200)
  • force_login() 跳过认证直接登录,比 login()

常用断言

断言 说明
assertEqual(a, b) 相等
assertRaises(Exception) 抛出异常
assertRedirects(response, url) 重定向到URL
assertTemplateUsed(response, template) 使用了模板
assertContains(response, text) 响应包含文本
python manage.py test                     # 运行所有测试
python manage.py test myapp               # 指定应用
python manage.py test --keepdb            # 保留测试数据库(加速)
python manage.py test --parallel=4        # 并行测试

十七、数据库迁移 Migrations

1. 基本命令

python manage.py makemigrations           # 检测model变化,生成迁移文件
python manage.py migrate                  # 执行迁移
python manage.py showmigrations           # 查看迁移状态
python manage.py sqlmigrate myapp 0001    # 查看迁移对应的SQL

迁移文件应该提交到版本控制,保证团队数据库结构一致

--fake 标记已执行但不运行SQL,慎用

2. 数据迁移

makemigrations --empty 生成空迁移文件,用 RunPython 做数据操作

from django.db import migrations

def create_initial_data(apps, schema_editor):
    # 用 apps.get_model 获取模型(确保版本正确)
    Category = apps.get_model("myapp", "Category")
    Category.objects.create(name="Electronics")

def remove_data(apps, schema_editor):
    Category = apps.get_model("myapp", "Category")
    Category.objects.filter(name="Electronics").delete()

class Migration(migrations.Migration):
    dependencies = [("myapp", "0001_initial")]

    operations = [
        migrations.RunPython(create_initial_data, remove_data),
        # 第二个参数是反向操作(回退时执行)
    ]

关键:用 apps.get_model() 而非直接 import,保证使用的是该迁移时刻的模型版本

常见操作类型

操作 说明
CreateModel / DeleteModel 创建/删除表
AddField / RemoveField 添加/删除字段
AlterField 修改字段
RenameField / RenameModel 重命名
AddIndex / RemoveIndex 添加/删除索引
RunPython 执行Python代码
RunSQL 执行原始SQL

十八、异步支持

1. 异步视图

async def 定义,适合 I/O 密集型场景(调外部API、消息队列)

import asyncio
import httpx
from django.http import JsonResponse

async def fetch_multiple(request):
    async def fetch(url):
        async with httpx.AsyncClient() as client:
            return await client.get(url)

    urls = ["https://api1.com", "https://api2.com", "https://api3.com"]
    # 并发请求(同步视图做不到)
    responses = await asyncio.gather(*[fetch(url) for url in urls])
    return JsonResponse({"count": len(responses)})

2. sync_to_async 桥接

异步视图中不能直接调用同步ORM,会阻塞事件循环

from asgiref.sync import sync_to_async

async def async_view(request):
    # ❌ 错误:会阻塞事件循环
    # products = Product.objects.all()

    # ✅ 正确:用 sync_to_async 包装
    products = await sync_to_async(list)(Product.objects.all()[:10])
    return JsonResponse({"count": len(products)})

3. 异步 ORM(Django 4.1+)

方法名以 a 前缀,可以直接 await

async def async_orm_view(request):
    count = await Product.objects.acount()
    product = await Product.objects.afirst()
    product = await Product.objects.aget(pk=1)
    product = await Product.objects.acreate(name="New", price=100)

    # 异步迭代
    async for product in Product.objects.all():
        print(product.name)

    # 异步更新和删除
    await Product.objects.filter(stock=0).aupdate(is_active=False)
    return JsonResponse({"status": "ok"})

注意:异步视图中不能调用同步ORM方法,反之亦然


附:常用第三方包

用途
djangorestframework REST API
django-filter 查询过滤
django-cors-headers CORS跨域
drf-spectacular API文档
celery 异步任务队列
django-redis Redis缓存
django-storages 云存储(S3)
django-allauth 第三方登录
django-debug-toolbar 调试工具
django-extensions 扩展命令
django-environ 环境变量管理
django-crispy-forms 表单美化
django-htmx HTMX集成