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 里,还能用在其他场景:

  • 点(.)是获取对象
  • 双下划线(__)是穿越外键去取字段名或做条件。它只在 filtervaluesorder_by 这些方法里有效。
  • 如果你没写 related_name,反向双下划线要写成 author__books__title(因为默认是 book_set,但一般不用,推荐总是写 related_name)。
posted @ 2026-06-29 11:43  李成敏  阅读(9)  评论(0)    收藏  举报