Django中queryset.union返回异常查询集:filter()与get()返回全部内容
嘿,我之前也踩过这个坑!你用union()合并两个QuerySet后,调用filter()或者get()得不到预期结果,核心原因是Django的UnionedQuerySet有个容易忽略的限制:当你在合并后的QuerySet上添加过滤条件时,Django会把这个条件应用到每一个参与合并的子QuerySet上,再合并结果,而不是先合并所有数据再过滤。
看你的例子,bar.filter(pk=0)实际上是先给obj1.father.all()和obj2.father.all()分别加上pk=0的过滤,再把两个结果合并——这和你预期的“先合并再筛选”逻辑完全不一样,才会出现返回所有结果的奇怪情况。
那该怎么解决?推荐用下面两种更可靠的方法替代union():
方法一:用Q对象直接构造单一QuerySet(推荐)
既然你是要获取属于obj1或obj2的所有Link记录,完全可以直接从Link模型出发,用Q对象把两个条件合并成一个数据库查询,这样得到的是普通的QuerySet,支持所有正常操作:
from django.db.models import Q # 直接查询属于obj1或obj2的Link bar = Link.objects.filter(Q(father=obj1) | Q(father=obj2))
这样你再调用bar.filter(pk=0),就会正确返回只有pk=0的那条记录。而且这个方法是数据库层面的单一查询,性能比union()更好,还支持惰性求值、排序等所有QuerySet的特性。
方法二:转换成列表合并(适合复杂场景)
如果你的两个QuerySet是经过复杂过滤后得到的(比如已经做了很多其他条件的筛选,没法直接用Q对象重构),可以把它们转换成列表后合并,不过这样会失去QuerySet的惰性,数据会立即加载到内存:
# 合并两个QuerySet的列表 bar_list = list(foo) + list(obj2.father.all()) # 如果需要去重(避免重复记录),可以这样做: seen_pks = set() bar_unique = [] for link in bar_list: if link.pk not in seen_pks: seen_pks.add(link.pk) bar_unique.append(link)
需要注意的是,用set去重可能会打乱顺序,而且如果模型实例不可哈希会报错,所以上面的循环去重更稳妥。
什么时候该用union()?
union()其实更适合合并不同模型但字段结构相同的QuerySet,或者你需要严格去重且不需要后续过滤的场景。对于同模型的QuerySet合并,用Q对象的方式是最优解。
内容的提问来源于stack exchange,提问作者Elio Maisonneuve

