当前位置: 首页 > news >正文

Django ORM 2. 模型(Model)操作

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 Userclass 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_namedef __str__(self):return self.nameclass 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_namedef __str__(self):return self.nameclass 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_namedef __str__(self):return self.nameclass 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_namedef __str__(self):return self.titleclass 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_nameclass 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 Fakerprint("😁 开始初始化数据...")# 设置 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 常用的操作,建议有基础的读者先尝试自行完成下面的练习题,再对照答案解析。

注意:由于是随机生成的数据,实际练习时请根据生成的数据灵活变通

练习题

🧩 一、对象创建

  1. 创建一个新的出版社,名字为“博文出版社”,成立于 2010 年。

  2. 如果数据库中没有邮箱为 hello@example.com 的作者,则创建她,名字为“林芳”,性别为女。

  3. 一次创建 3 个作者,分别为“周杰”、“刘涛”、“何静”,声望3.0,邮箱自定义,性别随机。

🧩 二、对象删除

  1. 删除名字为“何静”的作者。

  2. 批量删除声望小于 2.0 的作者。

🧩 三、对象更新

  1. 把名字为“周杰”的作者性别修改为“F”。

  2. 把所有邮箱以 @example.com 结尾的作者声望设为 5.0。

  3. 将 ID 为 1 的作者对象的名字改为“匿名用户”并保存,只更新该字段。

  4. 强制更新 ID 为 2 的作者对象(模拟并发)。

🧩 四、查询对象

  1. 查询所有作者。

  2. 查询性别为“女”的作者。

  3. 查询“声望”大于 4.0 且为活跃状态的作者。

  4. 获取邮箱为 li4@example.com 的作者对象。

  5. 获取 ID 最小的作者。

  6. 获取 ID 最大的作者。

🧩 五、查询字段

  1. 查询所有作者的 nameemail 字段(只要这两个字段)。

  2. 获取所有作者的邮箱列表(只返回邮箱)。

  3. 查询作者对象时,仅获取 name 字段,延迟加载其他字段。

🧩 六、字段查找表达式

  1. 查询所有名字中包含“张”的作者。

  2. 查询邮箱以 gmail.com 结尾的作者。

  3. 查询声望在 3.0 到 5.0 之间的作者。

  4. 查询 id 属于 [1, 2, 3, 5, 8] 的作者。

  5. 查询 email 字段为 null 的作者(虽然我们的脚本里默认不会有)。

🧩 七、排序、切片与去重

  1. 按声望降序排列所有作者。

  2. 获取声望最高的前 5 位作者。

  3. 查询图书表中所有出版社 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 中是一个非常重要的语法符号,它的作用包括:

  1. 字段查询:添加查询条件后缀(如 __gt__contains 等)

  2. 跨表查询:连接关联模型的字段(如 author__name

  3. 特殊查询:实现更复杂的查询逻辑

记住这个简单规则:每当我们想在查询中"跨越"关系或添加查询条件时,就使用双下划线

 双下划线 __用法:

分类语法示例说明等效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)检查是否为NULLWHERE 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_updateF() 表达式

✅ 练习题答案

# 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() 必须搭配 valuesvalues_list 使用才有意义,单独使用时通常没有实际效果,结果不变,主要原因如下:

  1. 模型实例的唯一性

    • 当查询返回完整模型实例时,每个实例代表数据库中的一行,即使内容相同,它们也是不同的对象实例

    • Django 默认使用主键区分不同实例,因此 distinct() 对完整模型实例无效

  2. SQL 层面的限制

    • 在 SQL 中,DISTINCT 作用于查询的所有选定字段

    • 当查询包含主键时(完整模型查询默认包含),每一行都因主键不同而自动"不同"

  • distinct() 在以下情况才有实际意义:

  1. 配合 values() 或 values_list() 查询特定字段

  2. 使用 order_by() 时注意字段选择(某些数据库(如PostgreSQL)要求 DISTINCT 字段必须包含在 ORDER BY 中)

  3. 跨多表关联查询时去除重复,因为JOIN 会引入重复数据,必须去重以防止分页错乱、数据重复

        例如:查询所有作者中,有图书的作者列表(不重复)

Author.objects.filter(books__isnull=False).distinct()

   AuthorBook 是多对多;

       若某作者有多本书,关联查询时该作者会重复出现多次;

   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. 进阶操作

练习题

🧩 一、关系查询

  1. 查询所有图书的出版社名称

  2. 查询名字为“张三”的作者所写的所有书名

  3. 查询图书《Python入门》的所有作者名字

  4. 查询出版社“博文出版社”出版的图书数量

  5. 查询用户"wanyang"对应档案中的电话

🧩 二、select_related 与 prefetch_related

  1. 查询所有图书及其出版社名称,尽量减少查询次数

  2. 查询所有图书及其作者姓名,避免重复查询

🧩 三、聚合与注解

  1. 所有图书的平均价格是多少?

  2. 每个出版社出版的图书数量

  3. 查询图书数量大于 5 的出版社

  4. 查询每位作者所写图书的总价格

🧩 四、Q 对象与复杂查询

  1. 查询名字为“张三”或“李四”的作者

  2. 查询活跃的作者,且邮箱是以 gmail.com 结尾

  3. 查询不是活跃状态的作者或声望小于 3 的作者

🧩 五、F 表达式与字段运算

  1. 所有图书价格增加 5 元

  2. 声望大于 3 的作者,声望加 1 分

  3. 将图书库存数量减 1(模拟下单)

🧩 一、关系查询

✅ 方法说明:

关系类型正向访问反向访问(默认)
ForeignKeybook.publisher.namepublisher.book_set.all()
ManyToManybook.authors.all()author.books.all()
OneToOneauthor.profile.cityprofile.author.name

🔍 注意事项

  • 反向关系字段可以自定义 related_name

  • 多对多 .all() 是必须的,表示是一个 QuerySet

  • OneToOne 访问关系就像属性一样自然

✅ 练习题答案

# 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

提高效率的原因

  1. 减少查询次数:将多个查询合并为一个

  2. 避免N+1查询问题:访问关联对象时不再需要额外查询

  3. 适合"多对一"和"一对一"关系

示例:获取书籍及其出版社(外键关系) 

# 普通查询: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']  # 直接从缓存中读取,不访问数据库

提高效率的原因

  1. 减少查询次数:从O(N)降到O(1)

  2. 批量获取:一次性获取所有关联对象而不是逐个获取

  3. 适合"多对多"和"一对多"关系

示例:获取作者及其所有书籍(多对多关系)

# 普通查询: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 或 b

  • Q(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

  1. 查询价格大于图书平均价格的图书

  2. 查询每位作者所写图书中价格最高的书名

  3. 查询至少出版过一本书的出版社(使用 Exists)

🧩 二、条件注解(Case / When)

  1. 标记图书价格是否高于 100 元,字段为 is_expensive

  2. 标记作者声望是否大于 4,命名为 level,值为 "高"/"低"

🧩 三、分组统计与聚合结合

  1. 查询每位作者的图书数量和平均价格

  2. 查询每位出版社中价格最高的图书名

🧩 四、原生 SQL(raw)

  1. 使用 raw SQL 查询价格超过 100 的图书

  2. 使用原生 SQL 查询作者及其对应图书数量(带 JOIN)

🧩 五、自定义 QuerySet 和 Manager

  1. 自定义方法:获取所有高声望作者(reputation > 4)

🧩 六、select_for_update 与事务控制

  1. 使用事务安全地减少图书库存(下单扣库存)

  2. 对作者做更新时加锁,确保并发写入安全

🧩 一、Subquery / OuterRef / Exists

✅ 方法说明:

关键词作用
Subquery子查询,用来插入一个完整的“子查询”结果,通常用在 .annotate() 中,用来加字段
OuterRef外层引用,在子查询中引用外层模型字段,是 SubqueryExists 的“桥梁
Exists存在判断,判断子查询是否有结果(用于过滤),通常用在 .filter() 中,用来筛选数据

Subquery + OuterRef 

每个作者最新出版的一本书名:对于每个作者,从他的书中找一本“出版时间最新的书”,拿出它的标题。

🧾 SQL 写法:

SELECT a.*, (SELECT b.title FROM book bWHERE b.author_id = a.idORDER BY b.published_date DESCLIMIT 1
) AS latest_book
FROM author a;

🐍 Django ORM 写法:

from django.db.models import OuterRef, Subquerylatest_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 传进子查询中

  • SubqueryExists 可以因此对子查询进行“按行个性化”

Exists 

筛选有书的作者:只要某个作者写过至少一本书,就选中他。

🧾 SQL 写法:

SELECT * FROM author a
WHERE EXISTS (SELECT 1FROM book_authors baWHERE 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'),需用 Subquery

  • annotate() 会改变 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 aLEFT JOIN author_books ab ON a.id = ab.author_idLEFT JOIN book b ON ab.book_id = b.idGROUP 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 -= 1book.save()# 2. 锁定作者更新
with transaction.atomic():author = Author.objects.select_for_update().get(id=2)author.name = "并发安全"author.save()

相关文章:

  • 机器学习7——神经网络上
  • 高频SQL50题 第九天 | 1164. 指定日期的产品价格、1204. 最后一个能进入巴士的人、1907. 按分类统计薪水
  • pytorch--模型训练的一般流程
  • 1 Studying《Computer Vision: Algorithms and Applications 2nd Edition》11-15
  • MySQL之全场景常用工具链
  • MYSQL与PostgreSQL的差异
  • (Arxiv-2025)Qwen2.5-VL 技术报告
  • mybatis-plus从入门到入土(一):快速开始
  • Embedding模型微调实战(ms-swift框架)
  • 医疗AI智能基础设施构建:向量数据库矩阵化建设流程分析
  • 领域驱动设计(DDD)【28】之实践或推广DDD的学习
  • 左神算法之矩阵旋转90度
  • <STC32G12K128入门第二十二步>STC32G驱动DS18B20(含代码)
  • IDE/IoT/实践小熊派LiteOS工程配置、编译、烧录、调试(基于 bearpi-iot_std_liteos 源码)
  • 2025.1版本PyCharam找不到已存在的conda虚拟环境
  • 领域驱动设计(DDD)【27】之CQRS四个层面的策略
  • Ubuntu服务器(公网)- Ubuntu客户端(内网)的FRP内网穿透配置教程
  • Spring Cloud 服务追踪实战:使用 Zipkin 构建分布式链路追踪
  • Python爬虫:Requests与Beautiful Soup库详解
  • MATLAB变音系统设计:声音特征变换(男声、女声、童声互转)