django orm中的跨表查询
两个核心概念:正向查询与反向查询
- 正向查询:从包含外键字段的模型触发,访问其关联的模型(从“多”查“一”)
- 反向查询:从被外键关联的模型触发,访问关联它的模型(从“一”查”多“)
以下查询的模型:
from django.db import models
class Author(models.Model):
name = models.CharField('姓名', max_length=50)
age = models.IntegerField('年龄')
class Book(models.Model):
title = models.CharField('书名', max_length=100)
# 外键放在 Book 里,Book 是“多”,Author 是“一”
author = models.ForeignKey(
Author,
on_delete=models.CASCADE,
related_name='books' # 给反向查询起个别名,方便理解
)
基于对象的跨表查询
正向查询(从“多”查“一”)
这种方式的核心理念是:我先拿到一个模型实例(对象),然后通过“点(.)”这个对象去访问它的关联对象。
通过书查作者:
# 1. 先拿到一本书的对象
book = Book.objects.get(id=1)
# 2. 通过 .author 直接拿到作者对象(正向查询)
author = book.author
print(author.name) # 输出作者名字
print(author.age) # 输出作者年龄
底层逻辑:当你执行 book.author 时,Django 会立刻去数据库执行一条 SELECT ... FROM author WHERE id = ... 的 SQL 语句。
反向查询(从“一”查“多”)
因为外键不在 Author 里,所以不能直接 .book。Django 会自动生成一个反向管理器。
- 默认名称:
小写模型名_set,即book_set。 - 自定义名称:因为我们在外键里写了
related_name='books',所以用books。
查询作者的所有书籍:
# 1. 先拿到一个作者对象
author = Author.objects.get(id=1)
# 2. 通过 .books 拿到这个作者写的所有书籍(这是一个 QuerySet)
books = author.books.all()
# 3. 遍历或进一步过滤
for book in books:
print(book.title)
# 反向查询也可以继续加条件(返回的还是 QuerySet)
django_books = author.books.filter(title__icontains="Django")
django_books = author.book_set.fileter(title__icontains="Django") # 如果未定义related_name
注意的问题:
(1)缓存机制:当你第一次执行 book.author 时,Django 会查询数据库,并把结果存在 book 对象内部的 _author_cache 里。第二次再调用 book.author 时,Django 不会再查数据库,直接使用缓存。
book = Book.objects.get(id=1)
print(book.author.name) # 执行了 SQL 查询
print(book.author.age) # 没有执行 SQL,直接从缓存拿!
(2)致命的 N+1 问题
# 假设有 10 本书
books = Book.objects.all() # 执行第 1 次 SQL:查询出 10 本书
for book in books:
# 循环内,每本书都要查一次作者
print(book.author.name) # 循环 10 次,执行了 10 次 SQL
总查询次数 = 1(查书) + 10(查作者) = 11 次。如果数据量很大,网站速度会变得极慢。解决这个问题的方案是 select_related
基于双下划线的跨表查询
这种方式的核心理念是:我不关心怎么拿到对象,我只关心“过滤条件”和“要什么数据”。 我用 __(双下划线)来告诉 Django:“我要顺着这个外键关系,去关联表里找某个字段做比较。”
在双下划线的查询中,正向查询的时候是:外键字段__关联模型字段名(author__name);反向查询的时候,在没有related_name的情况下是:模型名字小写_set__有外键模型的字段名(book_set__title)
重点:它永远用在 filter()、exclude()、get()、order_by()、values() 等查询集方法的内部。
1. 正向跨表过滤(从“多”查“一”的字段)
需求:查询 作者名字叫“张三” 的所有书籍。
# 普通思维:先找张三,再找他写的书(需要两步)
# 双下划线思维:一步到位!
books = Book.objects.filter(author__name="张三")
# 解析:author__name 表示 “找到 Book 关联的 Author,然后取 Author 的 name 字段做比较”
你甚至可以连续穿越多层(比如书 -> 作者 -> 出版社,如果有的话):filter(author__publisher__name="清华")。
2. 反向跨表过滤(从“一”查“多”的字段)
需求:查询 写过书名包含“Python”的书 的所有作者。
# 注意:这里的主表是 Author,我们反着去查 Book 表的 title 字段
authors = Author.objects.filter(books__title__icontains="Python")
authors = Author.objects.fileter(book_set_title__icontains="Python") # 在没有related_name的情况下
# 解析:books 是我们定义的 related_name,__title 是穿越过去取 Book 的 title 字段
3. 不只是过滤!还能用于排序和取值
双下划线不仅能用在 filter 里,还能用在其他场景:
- 排序:把书籍按作者名字的字母顺序排列。
Book.objects.order_by('author__name')
- 只取特定字段(values):获取所有书籍的书名以及对应的作者名。
# 返回一个字典列表,直接跨表取字段
data = Book.objects.values('title', 'author__name')
4. 反向查询的空值处理(isnull)
如果外键允许为空(null=True),你可以用双下划线查出所有没有作者的书:
books_without_author = Book.objects.filter(author__isnull=True)
基于对象与基于双下划线查询的区别
| 对比维度 | 基于对象(点语法) | 基于双下划线(__) |
|---|---|---|
| 查询时机 | 先查主表,拿到对象后,再发请求去查关联表(至少2次SQL)。 | 一次 SQL 搞定(利用 JOIN 连接表),只查一次数据库。 |
| 返回结果 | 返回模型实例对象(如 Author 对象)或 QuerySet。 |
永远返回 QuerySet(除非用 get)。 |
| 主要用途 | 获取关联的数据(我要看这本书的作者是谁)。 | 筛选数据(我要找作者叫张三的书)。 |
| 能否链式调用 | 不能,拿到对象就结束了。 | 可以,filter(...).filter(...).order_by(...)。 |
| 性能风险 | 极易产生 N+1 查询,循环里用很危险。 | 只要写出来,就是 1 次查询,性能好。 |
注意
1. 看需求决定用哪个
- 如果你的最终目的是 “展示/获取” 关联对象的详细数据(比如在模板里
{{ book.author.name }}),用 基于对象(点语法),但必须配合select_related优化。 - 如果你的最终目的是 “筛选/查找” 符合特定条件的数据(比如“找出所有 2023年出版的书的作者”),用 基于双下划线。
2. 千万别在循环里用点语法!
# ❌ 绝对禁止的写法(11次查询)
for book in Book.objects.all():
print(book.author.name)
# ✅ 改进写法(如果非要用点,提前用 select_related 预加载,2次查询)
for book in Book.objects.select_related('author').all():
print(book.author.name)
3. 记住双下划线不是“点”:
双下划线不仅能用在 filter 里,还能用在其他场景:
- 点(
.)是获取对象。 - 双下划线(
__)是穿越外键去取字段名或做条件。它只在filter、values、order_by这些方法里有效。
4. 反向查询时必须用 related_name 或 小写模型名_set
- 如果你没写
related_name,反向双下划线要写成author__books__title(因为默认是book_set,但一般不用,推荐总是写related_name)。

浙公网安备 33010602011771号