Django ORM 2. 模型(Model)操作

发布于:2025-07-01 ⋅ 阅读:(19) ⋅ 点赞:(0)

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 常用的操作,建议有基础的读者先尝试自行完成下面的练习题,再对照答案解析。

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

练习题

🧩 一、对象创建

  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) 检查是否为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_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(模拟下单)

🧩 一、关系查询

✅ 方法说明:

关系类型 正向访问 反向访问(默认)
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() 是必须的,表示是一个 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 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 传进子查询中

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

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'),需用 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 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()

网站公告

今日签到

点亮在社区的每一天
去签到