1. 数据准备
本文后续所有 ORM 操作将通过提供通用的测试数据进行演示:
定义 6 个模型(覆盖字段类型、关系类型、查询、聚合、注解等场景)
关于创建模型请参考上一节:Django ORM 1. 创建模型(Model)
使用 Django 离线脚本批量生成各模型测试数据
模型定义
在测试app(假如app名为web)下的models.py(web/models.py)中添加如下6个模型:
from django.db import models
from django.contrib.auth.models import User
class Category(models.Model):
name = models.CharField(max_length=50, verbose_name='分类名称')
slug = models.SlugField(unique=True, verbose_name='URL Slug')
class Meta:
db_table = 'category'
verbose_name = '分类'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Author(models.Model):
name = models.CharField(max_length=100, verbose_name='姓名')
gender = models.CharField(max_length=1, choices=[('M', '男'), ('F', '女')], verbose_name='性别')
email = models.EmailField(unique=True, verbose_name='邮箱')
bio = models.TextField(null=True, blank=True, verbose_name='简介')
is_active = models.BooleanField(default=True, verbose_name='是否活跃')
joined_at = models.DateTimeField(auto_now_add=True, verbose_name='注册时间')
reputation = models.FloatField(default=0.0, verbose_name='声望')
class Meta:
db_table = 'author'
verbose_name = '作者'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Publisher(models.Model):
name = models.CharField(max_length=100, verbose_name='出版社名称')
address = models.TextField(null=True, blank=True, verbose_name='地址')
founded_year = models.IntegerField(verbose_name='成立年份')
website = models.URLField(null=True, blank=True, verbose_name='官网')
class Meta:
db_table = 'publisher'
verbose_name = '出版社'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200, verbose_name='书名')
slug = models.SlugField(unique=True, verbose_name='Slug')
description = models.TextField(null=True, blank=True, verbose_name='简介')
price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='价格')
rating = models.FloatField(verbose_name='评分')
is_published = models.BooleanField(default=True, verbose_name='是否发布')
published_date = models.DateField(verbose_name='出版日期')
metadata = models.JSONField(default=dict, null=True, blank=True, verbose_name='元数据')
category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, related_name='books', verbose_name='分类')
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, related_name='books', verbose_name='出版社')
authors = models.ManyToManyField(Author, related_name='books', verbose_name='作者')
class Meta:
db_table = 'book'
verbose_name = '图书'
verbose_name_plural = verbose_name
def __str__(self):
return self.title
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, verbose_name='用户')
avatar = models.ImageField(upload_to='avatars/', null=True, blank=True, verbose_name='头像')
phone = models.CharField(max_length=20, null=True, blank=True, verbose_name='电话')
address = models.TextField(null=True, blank=True, verbose_name='地址')
preferences = models.JSONField(default=dict, null=True, blank=True, verbose_name='偏好设置')
class Meta:
db_table = 'user_profile'
verbose_name = '用户资料'
verbose_name_plural = verbose_name
class Review(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='reviews', verbose_name='图书')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='用户')
rating = models.PositiveIntegerField(verbose_name='评分')
comment = models.TextField(null=True, blank=True, verbose_name='评论内容')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='评论时间')
is_approved = models.BooleanField(default=False, verbose_name='是否通过审核')
class Meta:
db_table = 'review'
verbose_name = '评论'
verbose_name_plural = verbose_name
模型关系
模型 | 关系类型 | 被关联模型 | 说明 |
---|---|---|---|
Category | 一对多 | Book | 一个分类可以有多个书籍,一本书只能有一个分类 |
Publisher | 一对多 | Book | 一个出版社可以出版多本书籍,一本书只能有一个出版社 |
Author | 多对多 | Book | 一个作者可以有多本书,一本书可以有多个作者 |
Book | 一对多 | Review | 一本书可以有多个评论,一个评论只能属于一本书 |
User | 一对多 | Review | 一个用户可以写多个评论,一个评论只能由一个用户创建 |
User | 一对一 | UserProfile | 一个用户对应一个用户资料(一对一) |
User 与 UserProfile:一对一扩展用户信息,是管理用户偏好、头像、电话等常见实践。
Book 是核心实体:关联分类(分类统计)、出版社(聚合分析)、作者(多对多处理)、评论(用户行为分析)等操作。
Review 是行为数据:用于演示聚合、分组、条件注解等复杂 ORM 操作。
Author 与 Book 多对多:展示复杂关系字段查询、跨表预加载优化(
prefetch_related
)等。
离线数据准备
Django 离线数据脚本是一种在 Django 项目之外或独立于常规请求-响应循环运行的 Python 脚本,用于批量添加、更新或处理模型数据。这类脚本通常用于数据迁移、初始化数据或批量数据处理等场景。可以独立运行,不需要启动 Django 开发服务器。
基本实现方式:
setdefault
确保 Django 知道使用哪个设置模块django.setup()
完成以下关键操作:初始化 Django 配置
设置日志记录
准备应用注册表
加载模型(非常重要)
import os
import django
# 设置环境变量指向你的 settings 模块
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'yourproject.settings')
# 加载 Django 配置
django.setup()
使用 Faker 生成测试数据
Faker 是一个强大的 Python 库,专门用于生成各种类型的假数据(fake data)
安装faker
pip install faker
初始化 Faker
from faker import Faker
# # 创建 Faker 实例,指定中文(默认为英文)
fake = Faker('zh_CN')
常用数据生成方法
fake.name() # 姓名
fake.first_name() # 名
fake.last_name() # 姓
fake.email() # 邮箱
fake.phone_number() # 电话号码
fake.address() # 地址
fake.job() # 职位
fake.company() # 公司名
fake.word() # 单词
fake.sentence() # 句子
fake.paragraph() # 段落
fake.text() # 文本
fake.random_int(min=0, max=100) # 整数
fake.pyfloat() # 浮点数
fake.date_time() # 日期时间
fake.date() # 日期
fake.time() # 时间
fake.url() # URL
fake.ipv4() # IPv4地址
fake.user_agent() # 用户代理
fake.color_name() # 颜色名称
fake.hex_color() # 十六进制颜色
fake.file_name() # 文件名
离线数据脚本
在项目根目录创建一个prepare_data.py文件,添加如下创建离线数据脚本:
"""
本脚本用于在 Django 项目外部运行,批量创建演示用数据。
使用前确保虚拟环境已激活,项目配置无误。
"""
import os
import django
import random
from decimal import Decimal
from faker import Faker
print("😁 开始初始化数据...")
# 设置 Django 配置环境变量
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'DjangoProject.settings') # 替换为你的项目设置模块
django.setup()
from django.contrib.auth.models import User
from web.models import Category, Author, Publisher, Book, Review, UserProfile
# 创建 Faker 实例,指定中文
fake = Faker('zh_CN')
# 清空旧数据,保证重复执行脚本不会出错
Review.objects.all().delete()
Book.objects.all().delete()
Author.objects.all().delete()
Publisher.objects.all().delete()
Category.objects.all().delete()
UserProfile.objects.all().delete()
User.objects.exclude(is_superuser=True).delete()
# 创建分类
categories = [
Category.objects.create(name=fake.word(), slug=fake.slug())
for _ in range(10)
]
# 创建出版社
publishers = [
Publisher.objects.create(
name=fake.company(),
address=fake.address(),
founded_year=random.randint(1950, 2020),
website=fake.url()
) for _ in range(10)
]
# 创建作者
authors = [
Author.objects.create(
name=fake.name(),
gender=random.choice(['M', 'F']),
email=fake.unique.email(),
bio=fake.text(),
is_active=random.choice([True, False]),
reputation=round(random.uniform(1.0, 5.0), 2)
) for _ in range(30)
]
# 创建用户及其用户资料
users = []
for _ in range(30):
user = User.objects.create_user(
username=fake.unique.user_name(),
email=fake.email(),
password='12345678'
)
users.append(user)
UserProfile.objects.create(
user=user,
avatar='',
phone=fake.phone_number(),
address=fake.address(),
preferences={'theme': random.choice(['dark', 'light'])}
)
# 创建图书并关联分类、出版社、作者
books = []
for i in range(30):
book = Book.objects.create(
title=fake.sentence(nb_words=4).rstrip('.'),
slug=fake.unique.slug(),
description=fake.text(),
price=Decimal(random.randint(30, 150)),
rating=round(random.uniform(1.0, 5.0), 2),
is_published=random.choice([True, False]),
published_date=fake.date_between(start_date='-3y', end_date='today'),
publisher=random.choice(publishers),
category=random.choice(categories),
metadata={'pages': random.randint(100, 500), 'language': 'zh-CN'}
)
# 随机分配 1~3 个作者
book.authors.set(random.sample(authors, random.randint(1, 3)))
books.append(book)
# 为每本书添加 3~7 条评论
for book in books:
for _ in range(random.randint(3, 7)):
Review.objects.create(
book=book,
user=random.choice(users),
rating=random.randint(1, 5),
comment=fake.sentence(),
is_approved=random.choice([True, False])
)
print("✅ 数据初始化完成")
2. 基础操作
练习先行,思维驱动学习
在接下来的操作章节中,我们将通过练习 + 批注解答的方式掌握 ORM 常用的操作,建议有基础的读者先尝试自行完成下面的练习题,再对照答案解析。
注意:由于是随机生成的数据,实际练习时请根据生成的数据灵活变通。
练习题
🧩 一、对象创建
创建一个新的出版社,名字为“博文出版社”,成立于 2010 年。
如果数据库中没有邮箱为
hello@example.com
的作者,则创建她,名字为“林芳”,性别为女。一次创建 3 个作者,分别为“周杰”、“刘涛”、“何静”,声望3.0,邮箱自定义,性别随机。
🧩 二、对象删除
删除名字为“何静”的作者。
批量删除声望小于 2.0 的作者。
🧩 三、对象更新
把名字为“周杰”的作者性别修改为“F”。
把所有邮箱以
@example.com
结尾的作者声望设为 5.0。将 ID 为 1 的作者对象的名字改为“匿名用户”并保存,只更新该字段。
强制更新 ID 为 2 的作者对象(模拟并发)。
🧩 四、查询对象
查询所有作者。
查询性别为“女”的作者。
查询“声望”大于 4.0 且为活跃状态的作者。
获取邮箱为
li4@example.com
的作者对象。获取 ID 最小的作者。
获取 ID 最大的作者。
🧩 五、查询字段
查询所有作者的
name
和email
字段(只要这两个字段)。获取所有作者的邮箱列表(只返回邮箱)。
查询作者对象时,仅获取
name
字段,延迟加载其他字段。
🧩 六、字段查找表达式
查询所有名字中包含“张”的作者。
查询邮箱以
gmail.com
结尾的作者。查询声望在 3.0 到 5.0 之间的作者。
查询 id 属于 [1, 2, 3, 5, 8] 的作者。
查询 email 字段为 null 的作者(虽然我们的脚本里默认不会有)。
🧩 七、排序、切片与去重
按声望降序排列所有作者。
获取声望最高的前 5 位作者。
查询图书表中所有出版社 ID 的去重结果。
🧩 一、对象创建
objects对象
objects
是 Django 模型默认管理器(Manager)的实例,它是模型与数据库之间的"中间人"。通过它我们可以对数据库进行各种操作。它就像是 Django 模型的一个"万能工具箱":
不用写SQL:帮我们把Python代码转换成SQL语句
安全:自动防止SQL注入攻击
方便:提供链式调用等便捷方法
✅ 方法说明:
方法 | 功能 | 适用场景 |
---|---|---|
create() |
创建单个对象,立即写入数据库 | 表单提交、普通接口写入 |
get_or_create() |
有则取,无则新建 | 去重导入、同步第三方用户等场景 |
bulk_create() |
一次性创建多个对象,性能极高 | 脚本初始化、批量导入 |
✅ 练习题答案
# 1. 创建一个出版社
Publisher.objects.create(name='博文出版社', founded_year=2010)
# 2. 有则获取,无则创建
Author.objects.get_or_create(
email='hello@example.com',
defaults={'name': '林芳', 'gender': 'F'}
)
# 3. 批量创建作者
Author.objects.bulk_create([
Author(name='周杰', reputation=3.0, email='zhoujie@example.com', gender='M'),
Author(name='刘涛', reputation=3.0, email='liutao@example.com', gender='F'),
Author(name='何静', reputation=3.0, email='hejing@example.com', gender='F'),
])
🧩 二、对象删除
双下划线 __
双下划线 __
在 Django 中是一个非常重要的语法符号,它的作用包括:
字段查询:添加查询条件后缀(如
__gt
,__contains
等)跨表查询:连接关联模型的字段(如
author__name
)特殊查询:实现更复杂的查询逻辑
记住这个简单规则:每当我们想在查询中"跨越"关系或添加查询条件时,就使用双下划线。
双下划线 __用法:
分类 | 语法示例 | 说明 | 等效SQL示例 |
---|---|---|---|
字段查询条件 | |||
精确匹配 | filter(title__exact='Python') |
精确匹配 (通常可以省略__exact ) |
WHERE title = 'Python' |
包含 | filter(title__contains='Django') |
区分大小写的包含 | WHERE title LIKE '%Django%' |
开头匹配 | filter(title__startswith='Py') |
以指定字符串开头 | WHERE title LIKE 'Py%' |
结尾匹配 | filter(title__endswith='on') |
以指定字符串结尾 | WHERE title LIKE '%on' |
不区分大小写 | filter(title__iexact='python') |
不区分大小写的精确匹配 | WHERE title ILIKE 'python' |
大于/小于 | filter(price__gt=100) |
gt(>), gte(>=), lt(<), lte(<=) | WHERE price > 100 |
范围 | filter(price__range=(50,100)) |
值在范围内 | WHERE price BETWEEN 50 AND 100 |
空值检查 | filter(desc__isnull=True) |
检查是否为NULL | WHERE desc IS NULL |
日期查询 | |||
年/月/日 | filter(date__year=2023) |
按年/月/日过滤 (__month , __day ) |
WHERE EXTRACT(YEAR FROM date)=2023 |
周/季度 | filter(date__week=52) |
按周/季度过滤 (__quarter ) |
WHERE EXTRACT(WEEK FROM date)=52 |
关联模型查询 | |||
正向关联 | filter(author__name='John') |
通过外键关系查询关联模型的字段 | JOIN author ON ... WHERE author.name='John' |
反向关联 | filter(book__title='Python') |
通过反向关系查询 (如Author查询其Book) | JOIN book ON ... WHERE book.title='Python' |
多级关联 | filter(author__publisher__name='A') |
跨多级关系查询 | 多表JOIN后WHERE条件 |
聚合与注解 | |||
计数 | annotate(book_count=Count('book__id')) |
关联模型计数 | COUNT(book.id) |
平均值 | annotate(avg_price=Avg('book__price')) |
计算关联字段平均值 | AVG(book.price) |
特殊查询 | |||
JSON字段查询 | filter(data__key='value') |
查询JSON字段中的键值 | WHERE data->>'key' = 'value' |
数组包含 | filter(tags__contains=['python']) |
查询数组字段包含指定元素 (PostgreSQL) | WHERE tags @> ARRAY['python'] |
全文搜索 | filter(title__search='web') |
全文搜索 (依赖数据库后端) | WHERE to_tsvector(title) @@ to_tsquery('web') |
✅ 方法说明:
方法 | 用法说明 |
---|---|
obj.delete() |
删除当前对象 |
QuerySet.delete() |
一次性删除符合条件的多条记录 |
🔍 注意事项:
批量删除前一定要确保查询条件正确,避免误删数据
模型中有
is_deleted
逻辑字段时,建议使用逻辑删除替代物理删除删除外键时注意是否有
on_delete=models.CASCADE
,避免误删级联数据
✅ 练习题答案
# 1. 删除名字为“何静”的作者
Author.objects.filter(name='何静').delete()
# 2. 删除声望小于 2 的作者(批量)
Author.objects.filter(reputation__lt=2.0).delete()
🧩 三、对象修改
✅ 方法说明:
方法 | 用法示例 | 特点与建议 |
---|---|---|
obj.save() |
对象级别更新(触发信号) | 精细控制、逻辑校验、信号调用 |
obj.save(update_fields=[...]) |
只更新指定字段,提高效率 | 节省 SQL,避免不必要字段更新 |
obj.save(force_update=True) |
强制执行 UPDATE ,对象必须存在 |
避免并发覆盖(慎用) |
Model.objects.update(...) |
批量更新,效率最高 | 不触发信号,适合后台/管理任务 |
🔍 注意事项:
表单提交:推荐
save(update_fields=[...])
,只改动变动字段注意auto_now
字段是否需要更新:如果auto_now
字段(例如updated_at)没有被包含在update_fields
列表中,该字段(更新时间)不会自动更新。后台批量处理:推荐
update()
,避免遍历循环并发下乐观锁模拟:可结合
force_update
或F()
表达式
✅ 练习题答案
# 1. 更新作者性别
Author.objects.filter(name='周杰').update(gender='F')
# 2. 批量修改邮箱结尾为 @example.com 的作者声望
Author.objects.filter(email__endswith='@example.com').update(reputation=5.0)
# 3. 只更新 ID 为 1 的作者的 name 字段
author = Author.objects.get(id=1)
author.name = '匿名用户'
author.save(update_fields=['name']) # ✅ 只更新 name 字段
# 4. 强制更新 ID 为 2 的作者(要求已存在,否则抛异常)
author = Author.objects.get(id=2)
author.reputation += 0.1
author.save(force_update=True)
🧩 四、查询对象
✅ 方法说明:
方法 | 功能 |
---|---|
all() |
查询所有数据,返回QuerySet |
filter() |
传入多个条件,返回符合条件的 QuerySet |
exclude() |
过滤掉符合条件的记录,返回符合条件的QuerySet |
get() |
精确获取单个对象,失败会抛异常 |
first() / last() |
获取排序后的第一个/最后一个对象 |
🔍 注意事项:
get()
查询不到或返回多条都会抛异常,推荐用try...except
包裹QuerySet
是惰性查询,可链式拼接
✅ 练习题答案
# 1. 查询所有作者
Author.objects.all()
# 2. 查询性别为“女”的作者
Author.objects.filter(gender='F')
# 3. 声望大于 4.0 且活跃
Author.objects.filter(reputation__gt=4.0, is_active=True)
# 4. 获取指定邮箱的作者
Author.objects.get(email='li4@example.com')
# 5. 获取 ID 最小的作者
Author.objects.order_by('id').first()
# 6. 获取 ID 最大的作者
Author.objects.order_by('-id').first()
Author.objects.order_by('id').last()
🧩 五、查询字段
✅ 方法说明:
方法 | 返回类型 | 功能 |
---|---|---|
values() |
dict 列表 | 获取指定字段 |
values_list() |
tuple 列表 | 获取字段值元组,可设 flat=True |
only() |
模型对象 | 只预加载指定字段,访问其他字段时再查 |
defer() |
模型对象 | 推迟加载某些字段,加载除这些外的所有字段 |
🔍 注意事项:
only()
和defer()
会返回模型对象,适合性能优化。普通查询会加载所有字段,如果明确知道需要/不需要某些字段,才考虑用only()/dever()values()
/values_list()
更适合数据导出、API 返回、图表数据,实际开发中一般用list(Author.objects.values_list('name', flat=True))获取纯值列表
✅ 练习题答案
# 1. 获取 name 和 email 字段
Author.objects.values('name', 'email')
# 2. 获取邮箱列表
Author.objects.values_list('email', flat=True)
# 3. 只预加载 name 字段
Author.objects.only('name')
🧩 六、字段查找表达式
✅ 双下划线__
的主要用法说明:
表达式 | 含义 |
---|---|
contains / icontains |
字符串是否包含 |
startswith / endswith |
开头/结尾匹配 |
range(start, end) |
是否在某范围内 |
in=[…] |
是否在给定列表中 |
isnull=True/False |
是否为空 |
🔍 注意事项:
icontains
忽略大小写,适合用户输入模糊搜索isnull=True
不等于空字符串,指的是 NULL 值
✅ 练习题答案
# 1. 名字中包含“张”
Author.objects.filter(name__contains='张')
# 2. 邮箱以 gmail.com 结尾
Author.objects.filter(email__endswith='gmail.com')
# 3. 声望在 3~5 之间
Author.objects.filter(reputation__range=(3.0, 5.0))
# 4. ID 在指定列表中
Author.objects.filter(id__in=[1, 2, 3, 5, 8])
# 5. 邮箱字段为 null
Author.objects.filter(email__isnull=True)
🧩 七、排序、切片与去重
✅ 方法说明:
操作 | 含义与用途 |
---|---|
order_by() |
排序,默认升序,-字段 为降序 |
[:N] |
切片,等价于 SQL 的 LIMIT |
distinct() |
去重,只返回唯一值组合 |
🔍 注意事项:
排序字段建议加索引,防止性能问题
单表查询时distinct()
必须搭配values
或values_list
使用才有意义,单独使用时通常没有实际效果,结果不变,主要原因如下:
模型实例的唯一性:
当查询返回完整模型实例时,每个实例代表数据库中的一行,即使内容相同,它们也是不同的对象实例
Django 默认使用主键区分不同实例,因此
distinct()
对完整模型实例无效
SQL 层面的限制:
在 SQL 中,
DISTINCT
作用于查询的所有选定字段当查询包含主键时(完整模型查询默认包含),每一行都因主键不同而自动"不同"
distinct() 在以下情况才有实际意义:
配合
values()
或values_list()
查询特定字段使用
order_by()
时注意字段选择(某些数据库(如PostgreSQL)要求DISTINCT
字段必须包含在ORDER BY
中)跨多表关联查询时去除重复,因为JOIN 会引入重复数据,必须去重以防止分页错乱、数据重复
例如:查询所有作者中,有图书的作者列表(不重复)
Author.objects.filter(books__isnull=False).distinct()
Author
和 Book
是多对多;
若某作者有多本书,关联查询时该作者会重复出现多次;
distinct()
去重后只保留一条该作者记录。
是否需要用distinct()
查询类型 | 方向 | 示例 | 是否需要 .distinct() |
说明 |
---|---|---|---|---|
正向查询 | "多" → "一" | book.publisher Book.objects.select_related('publisher') |
❌ 不需要 | 直接获取单个关联对象 |
反向查询 | "一" → "多" | publisher.books.all() Publisher.objects.prefetch_related('books') |
❌ 不需要 | 返回关联的 QuerySet |
反向filter | "一" → "多" | Publisher.objects.filter(book__title="X") |
✅ 需要 | JOIN 操作可能导致重复 |
多对多查询 | "多" ↔ "多" | author.books.all() Book.objects.filter(authors__name="X") |
✅ 通常需要 | 多对多关系会产生重复 |
多对多filter | "多" ↔ "多" | Author.objects.filter(book__title="X") |
✅ 需要 | 通过中间表 JOIN 会产生重复 |
values() | 任意 | Book.objects.values('publisher__name') |
⚠️ 视情况而定 | 如果查询的字段组合可能重复,则需要 |
annotate() | 聚合查询 | Publisher.objects.annotate(book_count=Count('books')) |
❌ 通常不需要 | 聚合结果通常已经去重 |
✅ 练习题答案
# 1. 按声望降序排列
Author.objects.order_by('-reputation')
# 2. 声望前五名
Author.objects.order_by('-reputation')[:5]
# 3. 图书表中去重的出版社 ID
Book.objects.values_list('publisher_id', flat=True).distinct()
3. 进阶操作
练习题
🧩 一、关系查询
查询所有图书的出版社名称
查询名字为“张三”的作者所写的所有书名
查询图书《Python入门》的所有作者名字
查询出版社“博文出版社”出版的图书数量
查询用户"wanyang"对应档案中的电话
🧩 二、select_related 与 prefetch_related
查询所有图书及其出版社名称,尽量减少查询次数
查询所有图书及其作者姓名,避免重复查询
🧩 三、聚合与注解
所有图书的平均价格是多少?
每个出版社出版的图书数量
查询图书数量大于 5 的出版社
查询每位作者所写图书的总价格
🧩 四、Q 对象与复杂查询
查询名字为“张三”或“李四”的作者
查询活跃的作者,且邮箱是以
gmail.com
结尾查询不是活跃状态的作者或声望小于 3 的作者
🧩 五、F 表达式与字段运算
所有图书价格增加 5 元
声望大于 3 的作者,声望加 1 分
将图书库存数量减 1(模拟下单)
🧩 一、关系查询
✅ 方法说明:
关系类型 | 正向访问 | 反向访问(默认) |
---|---|---|
ForeignKey | book.publisher.name |
publisher.book_set.all() |
ManyToMany | book.authors.all() |
author.books.all() |
OneToOne | author.profile.city |
profile.author.name |
🔍 注意事项:
反向关系字段可以自定义
related_name
多对多
.all()
是必须的,表示是一个 QuerySetOneToOne 访问关系就像属性一样自然
✅ 练习题答案
# 1. 图书的出版社
for book in Book.objects.all():
print(book.title, book.publisher.name)
# 2. “张三”所写的图书
Author.objects.get(name='张三').books.all()
# 3. 图书《Python入门》的作者名
Book.objects.get(title='Python入门').authors.values_list('name', flat=True)
# 4. “博文出版社”的图书数量
Publisher.objects.get(name='博文出版社').book_set.count()
# 5. 用户“wanyang”的档案生日
User.objects.get(username='wanyang').userprofile.phone
🧩 二、select_related 与 prefetch_related
✅ 方法说明:
方法 | 用于关系类型 | 查询效率 |
---|---|---|
select_related() |
外键 / 一对一(ForeignKey / OneToOne) | 单条 JOIN 查询 ✅ |
prefetch_related() |
多对多 / 反向查询 | 多条子查询,内存合并 |
减少数据库查询次数,显著提升性能
✅ 若关联字段是 单个对象(如
ForeignKey
,OneToOne
),用select_related
;
✅ 若关联字段是 多个对象(如ManyToMany
, 反向外键),用prefetch_related
。
select_related
底层原理:SQL JOIN 操作
生成一个包含 JOIN 的 SQL 查询
一次性获取主表和相关联的外键表的所有数据
内存处理:
Django ORM 将JOIN结果转换为模型实例
建立模型间的引用关系,后续访问关联属性不再查询数据库
# 实际执行的SQL类似:
SELECT
book.id, book.title,...,
publisher.id, publisher.name, ...
FROM book
INNER JOIN publisher ON book.publisher_id = publisher.id
提高效率的原因:
减少查询次数:将多个查询合并为一个
避免N+1查询问题:访问关联对象时不再需要额外查询
适合"多对一"和"一对一"关系
示例:获取书籍及其出版社(外键关系)
# 普通查询:N+1问题(1次获取书籍 + N次获取出版社)
books = Book.objects.all()
for book in books:
print(book.publisher.name) # 每次循环都查询数据库
# 使用select_related:1次查询书籍及其出版社数据
books = Book.objects.select_related('publisher').all()
for book in books:
print(book.publisher.name) # 直接从book对象获取
prefetch_related
底层原理:分开查询 + Python级缓存
先执行主查询:
SELECT * FROM primary_table
收集所有关联ID:
SELECT * FROM related_table WHERE id IN (1,2,3,...)
内存处理:
ORM构建一个临时缓存字典:
{author_id: [book1, book2]}
当访问
author.books.all()
时直接从缓存读取
# 第一个查询获取所有作者
SELECT * FROM author
# 第二个查询获取这些作者的所有书
SELECT * FROM book WHERE author_id IN (1, 2, 3...)
# Django 使用 Prefetch 对象内部逻辑将查出的书籍进行分组缓存,大致过程如下:
author_id_to_books = defaultdict(list)
for book in all_books:
author_id_to_books[book.author_id].append(book)
# 让每个 Author 实例都带有自己的 _prefetched_objects_cache['books']
for author in all_authors:
# 手动设置缓存,避免再次访问数据库
setattr(author, '_prefetched_objects_cache', {'books': author_id_to_books[author.id]})
# 当访问 author.books.all() 时,Django 检查是否存在 _prefetched_objects_cache,因此不会触发sql查询
if 'books' in author._prefetched_objects_cache:
return author._prefetched_objects_cache['books'] # 直接从缓存中读取,不访问数据库
提高效率的原因:
减少查询次数:从O(N)降到O(1)
批量获取:一次性获取所有关联对象而不是逐个获取
适合"多对多"和"一对多"关系
示例:获取作者及其所有书籍(多对多关系)
# 普通查询:N+1问题
authors = Author.objects.all()
for author in authors:
print(author.books.all()) # 每次循环都查询数据库
# 使用prefetch_related:2次查询
authors = Author.objects.prefetch_related('books').all()
for author in authors:
print(author.books.all()) # 直接从缓存中获取
🔍 注意事项:
select_related()
更快,但只适用于“一对一”或“多对一”关系prefetch_related()
通用但略慢,适合多对多或反向外键(一对多)
✅ 练习题答案
# 1. 预加载出版社
Book.objects.select_related('publisher').all()
# 2. 预加载图书作者(多对多)
Book.objects.prefetch_related('authors').all()
🧩 三、聚合与注解
✅ 方法说明:
方法 | 作用 | 返回形式 |
---|---|---|
aggregate() |
聚合整张表 | 返回单条字典结果 |
annotate() |
为每条记录统计附加数据 | 返回包含附加字段的 QuerySet |
🔍 注意事项:
Count()
:统计数量Sum()
/Avg()
:求和、平均Max()
/Min()
:最大、最小
✅ 练习题答案
from django.db.models import Count, Avg, Sum
# 1. 所有图书平均价格
Book.objects.aggregate(avg_price=Avg('price'))
# 2. 每个出版社的图书数
Publisher.objects.annotate(book_count=Count('books'))
# 3. 出版图书超过 5 本的出版社
Publisher.objects.annotate(book_count=Count('books')).filter(book_count__gt=5)
# 4. 每位作者的图书总价
Author.objects.annotate(total_price=Sum('books__price'))
🧩 四、Q 对象与复杂查询
✅ 方法说明:
Q 对象支持构建复杂逻辑查询,如:
Q(a) | Q(b)
:表示 a 或 bQ(a) & Q(b)
:表示 a 且 b~Q(a)
:非 a(not)
🔍 注意事项:
使用 Q 对象时,必须用在
filter()
中。
✅ 练习题答案
from django.db.models import Q
# 1. 张三或李四
Author.objects.filter(Q(name='张三') | Q(name='李四'))
# 2. 活跃且邮箱以 gmail.com 结尾
Author.objects.filter(Q(is_active=True) & Q(email__endswith='gmail.com'))
# 3. 非活跃或声望 < 3
Author.objects.filter(Q(is_active=False) | Q(reputation__lt=3))
🧩 五、F 表达式与字段运算
✅ 方法说明:
F 表达式用于在 SQL 层进行字段值操作,不加载到 Python 内存中,整个计算过程发生在数据库服务器内部
字段与字段比较:
price__gt=F('cost_price')
批量运算更新:
update(stock=F('stock') - 1)
🔍 注意事项:
使用 F 表达式的场景:
排名计算、扣库存、累加积分等不需读取值的操作
✅ 练习题答案
from django.db.models import F
# 1. 图书价格 +5
Book.objects.update(price=F('price') + 5)
# 2. 声望 > 3 的作者声望 +1
Author.objects.filter(reputation__gt=3).update(reputation=F('reputation') + 1)
# 3. 扣库存
Book.objects.update(stock=F('stock') - 1)
4. 高级操作
练习题
🧩 一、Subquery / OuterRef / Exists
查询价格大于图书平均价格的图书
查询每位作者所写图书中价格最高的书名
查询至少出版过一本书的出版社(使用 Exists)
🧩 二、条件注解(Case / When)
标记图书价格是否高于 100 元,字段为
is_expensive
标记作者声望是否大于 4,命名为
level
,值为 "高"/"低"
🧩 三、分组统计与聚合结合
查询每位作者的图书数量和平均价格
查询每位出版社中价格最高的图书名
🧩 四、原生 SQL(raw)
使用 raw SQL 查询价格超过 100 的图书
使用原生 SQL 查询作者及其对应图书数量(带 JOIN)
🧩 五、自定义 QuerySet 和 Manager
自定义方法:获取所有高声望作者(reputation > 4)
🧩 六、select_for_update 与事务控制
使用事务安全地减少图书库存(下单扣库存)
对作者做更新时加锁,确保并发写入安全
🧩 一、Subquery / OuterRef / Exists
✅ 方法说明:
关键词 | 作用 |
---|---|
Subquery |
子查询,用来插入一个完整的“子查询”结果,通常用在 .annotate() 中,用来加字段 |
OuterRef |
外层引用,在子查询中引用外层模型字段,是 Subquery 和 Exists 的“桥梁” |
Exists |
存在判断,判断子查询是否有结果(用于过滤),通常用在 .filter() 中,用来筛选数据 |
Subquery + OuterRef
每个作者最新出版的一本书名:对于每个作者,从他的书中找一本“出版时间最新的书”,拿出它的标题。
🧾 SQL 写法:
SELECT a.*, (
SELECT b.title FROM book b
WHERE b.author_id = a.id
ORDER BY b.published_date DESC
LIMIT 1
) AS latest_book
FROM author a;
🐍 Django ORM 写法:
from django.db.models import OuterRef, Subquery
latest_book_qs = Book.objects.filter(authors=OuterRef('pk')).order_by('-published_date').values('title')[:1]
authors = Author.objects.annotate(latest_book_title=Subquery(latest_book_qs))
🚀 如何理解 OuterRef?
OuterRef('pk')
表示:“请引用外层(也就是 Author)当前行的主键”。相当于SQL中的WHERE b.author_id = a.id
它的作用是把“外层作者”这一行的 ID 传进子查询中
Subquery
或Exists
可以因此对子查询进行“按行个性化”
Exists
筛选有书的作者:只要某个作者写过至少一本书,就选中他。
🧾 SQL 写法:
SELECT * FROM author a
WHERE EXISTS (
SELECT 1
FROM book_authors ba
WHERE ba.author_id = a.id
);
🐍 Django ORM 写法
from django.db.models import Exists, OuterRef
from yourapp.models import Author, Book
# 构建子查询:查找是否存在至少一本书包含该作者,使用 .through 获取中间表
book_exists_qs = Book.authors.through.objects.filter(author_id=OuterRef('pk'))
# 主查询:只查出有书的作者
authors_with_books = Author.objects.annotate(
has_books=Exists(book_exists_qs)
).filter(has_books=True)
🔍 注意事项:
Subquery
必须返回单列OuterRef('字段')
是子查询中引用外层的唯一方式Exists
返回布尔值,可配合 filter 或 annotate
✅ 练习题答案
from django.db.models import Avg, Subquery, OuterRef, Exists
# 1. 查询价格大于图书平均价格
avg_price = Book.objects.aggregate(avg=Avg('price'))['avg']
Book.objects.filter(price__gt=avg_price)
# ✅ aggregate 适合全局统计。filter(price__gt=...) 支持直接比较。
# 2. 每位作者最贵图书名
sub = Book.objects.filter(authors=OuterRef('pk')).order_by('-price').values('title')[:1]
Author.objects.annotate(top_book=Subquery(sub))
# ✅ Subquery + OuterRef 可动态查出与当前作者相关的数据
# 3. 出版过图书的出版社
# 构建子查询:查找是否存在至少一本书包含该作者,使用 .through 获取中间表
book_exists_qs = Book.authors.through.objects.filter(author_id=OuterRef('pk'))
# 主查询:只查出有书的作者
authors_with_books = Author.objects.annotate(
has_books=Exists(book_exists_qs)
).filter(has_books=True)
🧩 二、条件注解(Case / When)
✅ 方法说明:
关键词 | 用途 |
---|---|
Case |
条件分支结构 |
When |
条件语句(相当于 if) |
Value |
条件成立时的返回值 |
🔍 注意事项:
必须指定
output_field
类型(CharField, IntegerField 等)多个 When 会依次匹配,第一个成立的为准
✅ 练习题答案
from django.db.models import Case, When, Value, IntegerField, CharField
# 1. 标记是否是比较贵的书
Book.objects.annotate(
is_expensive=Case(
When(price__gt=100, then=Value(1)),
default=Value(0),
output_field=IntegerField()
)
)
# ✅ 相当于在 SQL 中增加了 is_expensive 字段(1 或 0)
# 2. 声望等级
Author.objects.annotate(
level=Case(
When(reputation__gt=4, then=Value("高")),
default=Value("低"),
output_field=CharField()
)
)
🧩 三、分组统计与聚合结合
✅ 方法说明:
annotate()
可以结合聚合函数,如Count
,Avg
,Max
等Subquery + annotate
可实现“每组中某字段最大值的对应记录”
🔍 注意事项:
不支持直接
Max('books__price__title')
,需用 Subqueryannotate()
会改变 QuerySet 结构
✅ 练习题答案
from django.db.models import Count, Avg, Max
# 1. 作者图书数 + 平均价格
Author.objects.annotate(
book_count=Count('books'),
avg_price=Avg('books__price')
)
# 2. 每个出版社最贵图书名
sub = Book.objects.filter(publisher=OuterRef('pk')).order_by('-price').values('title')[:1]
Publisher.objects.annotate(max_price_title=Subquery(sub))
🧩 四、原生 SQL(raw)
✅ 方法说明:
Model.objects.raw(sql, params)
:适用于复杂 SQL 查询返回
RawQuerySet
,不可链式调用
🔍 注意事项:
必须手动指定字段别名与模型匹配
JOIN 查询不可自动反向转换为对象,需手动处理结果
✅ 练习题答案
# 1. 价格 > 100 的图书
books = Book.objects.raw("SELECT * FROM book WHERE price > %s", [100])
for book in books:
print(book.title, book.price)
# 2. 作者 + 图书数量统计
from django.db import connection
with connection.cursor() as cursor:
cursor.execute("""
SELECT a.id, a.name, COUNT(b.id)
FROM author a
LEFT JOIN author_books ab ON a.id = ab.author_id
LEFT JOIN book b ON ab.book_id = b.id
GROUP BY a.id
""")
for row in cursor.fetchall():
print(row)
🧩 五、自定义 QuerySet 和 Manager
✅ 方法说明:
自定义
QuerySet
适用于链式调用自定义
Manager
提供额外接口并包装 QuerySet
🔍 注意事项:
推荐组合使用,Manager 继承并封装 QuerySet
可重写
get_queryset()
过滤默认数据
✅ 练习题答案
为了同时支持
.filter(...).high_reputation()
链式调用 和Author.objects.high_reputation()
入口方法调用,我们必须分别在 QuerySet 和 Manager 中各写一个。在 QuerySet 中写一次逻辑,保证逻辑不重复,统一维护。
# models.py
class AuthorQuerySet(models.QuerySet):
def high_reputation(self):
return self.filter(reputation__gt=4)
class AuthorManager(models.Manager):
def get_queryset(self):
return AuthorQuerySet(self.model, using=self._db)
def high_reputation(self):
return self.get_queryset().high_reputation()
class Author(models.Model):
...
objects = AuthorManager() # 替换默认的模型管理器
# 1. 高声望作者
Author.objects.high_reputation()
🧩 六、select_for_update 与事务控制
✅ 方法说明:
select_for_update()
在事务中对行加锁,防止并发写入transaction.atomic()
确保数据库操作原子性
🔍 注意事项:
必须在事务环境中使用
select_for_update
并发写时推荐结合唯一索引、乐观锁补充处理
✅ 练习题答案
from django.db import transaction
# 1. 扣库存
with transaction.atomic():
book = Book.objects.select_for_update().get(id=1)
if book.stock > 0:
book.stock -= 1
book.save()
# 2. 锁定作者更新
with transaction.atomic():
author = Author.objects.select_for_update().get(id=2)
author.name = "并发安全"
author.save()