django笔记
一、入门基础
1. 环境安装与项目创建
推荐使用 Python 3.10+,先装好 Django 再创建项目
# 安装 Django
pip install django
# 创建项目(末尾的 . 表示在当前目录创建,避免多一层嵌套)
django-admin startproject myproject .
# 创建应用
python manage.py startapp myapp
项目创建后会在根目录生成
manage.py,这是日常开发最常用的命令入口应用创建后需在
settings.py的INSTALLED_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交互环境
makemigrations和migrate是一对,先 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"表示布尔标志,出现为 Trueself.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 |
ImageField和FileField的区别: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_urlAdmin 中"查看站点"按钮会用到
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', ...]
select_related / prefetch_related
解决 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 %}
运算符:
==!=<>innot inandornot
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 自动填充,适合 slugautocomplete_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.display的ordering指定点击列头排序时用的字段@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 生成四种权限:
addchangedeleteview格式:
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集成 |