在Django的ORM操作中,多对多关系的过滤是高频需求,比如文章模型关联了多个标签,需要筛选出所有关联了标签为Python或者Django的文章,这类需求的核心是判断主模型的关联对象中是否存在任意一个满足条件的情况。如果处理不当,很容易写出低效的查询代码,甚至引发性能问题。

基础模型定义
首先我们定义两个存在多对多关系的模型,后续所有示例都基于这两个模型展开:
from django.db import models
class Tag(models.Model):
name = models.CharField(max_length=50, verbose_name="标签名称")
def __str__(self):
return self.name
class Article(models.Model):
title = models.CharField(max_length=200, verbose_name="文章标题")
content = models.TextField(verbose_name="文章内容")
tags = models.ManyToManyField(Tag, related_name="articles", verbose_name="关联标签")
def __str__(self):
return self.title
使用filter直接过滤多对多字段
Django的ORM支持直接通过多对多字段的关联条件进行过滤,当我们需要筛选关联对象中任意一个满足条件时,直接对多对多字段的关联属性加过滤条件即可,ORM会自动处理关联查询的逻辑。
比如我们需要筛选出所有关联了名称为Python的标签的文章,代码如下:
# 筛选关联了标签名为Python的文章 articles = Article.objects.filter(tags__name="Python")
如果需要筛选关联了标签名为Python或者Django的文章,直接添加多个条件即可,ORM会自动处理为OR逻辑:
# 筛选关联了标签名为Python或者Django的文章 articles = Article.objects.filter(tags__name="Python", tags__name="Django")
这里需要注意,同一个filter中的多个条件针对的是同一个关联对象,上述代码实际是筛选同时关联了Python和Django两个标签的文章,如果需要筛选关联任意一个的情况,需要使用Q对象:
from django.db.models import Q # 筛选关联了Python或者Django标签的文章(任意一个满足即可) articles = Article.objects.filter(Q(tags__name="Python") | Q(tags__name="Django"))
使用prefetch_related优化查询
如果过滤之后还需要访问文章的关联标签数据,直接使用filter可能会产生额外的查询,这时候可以配合prefetch_related预加载关联数据,避免N加1查询问题。
# 过滤后预加载关联标签,避免后续访问标签时产生额外查询
articles = Article.objects.filter(
Q(tags__name="Python") | Q(tags__name="Django")
).prefetch_related("tags")
for article in articles:
# 这里访问tags不会再产生新的数据库查询
print(f"文章标题:{article.title},关联标签:{[tag.name for tag in article.tags.all()]}")
复杂条件的多对多过滤
如果过滤条件更复杂,比如需要筛选关联标签的创建时间在某个月份之后,或者标签名称包含特定字符,同样可以直接在filter中拼接条件:
import datetime # 筛选关联了2024年1月之后创建的标签的文章 target_date = datetime.date(2024, 1, 1) articles = Article.objects.filter(tags__create_time__gte=target_date)
如果需要先对关联对象做过滤再关联到主模型,也可以使用Prefetch对象自定义预加载的查询集:
from django.db.models import Prefetch
# 预加载时只加载名称为Python的标签
prefetch = Prefetch("tags", queryset=Tag.objects.filter(name="Python"))
articles = Article.objects.prefetch_related(prefetch).filter(tags__name="Python")
不同过滤方式性能对比
我们可以通过Django的query属性查看生成的SQL语句,对比不同写法的查询效率:
# 查看直接filter生成的SQL articles = Article.objects.filter(tags__name="Python") print(articles.query)
直接filter多对多字段的方式,ORM会生成带INNER JOIN的SQL语句,只查询一次数据库就能得到结果,性能最优。而如果使用先查询所有文章再遍历判断的方式,会产生大量额外的数据库查询,效率极低,不建议在生产环境使用。
注意事项
- 多对多过滤时,同一个filter中的多个条件默认是AND逻辑,需要OR逻辑时要使用Q对象。
- 如果过滤后不需要访问关联对象,不需要使用prefetch_related,避免额外的查询开销。
- 当多对多关系的中间表有额外字段时,过滤逻辑需要通过中间表模型来操作,不能直接通过主模型的多对多字段过滤。
多对多关系的过滤核心是利用Django ORM的关联查询能力,避免手动遍历判断,同时根据是否需要访问关联数据选择是否预加载,就能实现高效的查询逻辑。