Django ORM循环更新QuerySet后过滤返回空集及first()返回None问题排查
嘿,这是Django ORM的经典「缓存陷阱」!
我太懂这个坑了——你遇到的问题全是QuerySet的两个特性在搞鬼:惰性求值和结果缓存,咱们一步步说清楚:
为什么会出现这种怪事?
先唠QuerySet的两个“脾气”
- 懒到极致:惰性求值:QuerySet根本不会一创建就去查数据库,非得等你遍历它、调用
first()/last(),甚至只是打印它的时候,才会真正执行SQL查询。 - 记仇的缓存:结果缓存:第一次查完数据库,它会把结果存在自己的内存缓存里,之后你再访问这个QuerySet(比如再遍历、调
first()),它直接给你内存里的旧快照,绝对不会主动再去数据库刷新数据——除非你逼着它干。
拆解你的第一个代码示例
看这段代码:
qs = queryset.filter(status=models.BankTransfer.STATUS_NEW) for bank_transfer in qs: bank_transfer.status = models.BankTransfer.STATUS_APPROVED bank_transfer.save()
- 当你开始循环遍历
qs时,QuerySet终于“动起来”去数据库拉了所有status=NEW的记录,把它们存在内存缓存里。 - 你修改了每个对象的
status并保存到数据库——这时候数据库里的记录已经变成STATUS_APPROVED了,但qs内存里的缓存还是原来的对象(而且内存里的对象本身的status也被改成APPROVED了)。
现在你遇到的那些异常就说得通了:
- 过滤
qs返回空:你以为是在过滤原来的qs,但其实qs.filter(...)会生成一个全新的QuerySet,这个新家伙会直接去数据库查,而数据库里已经没有STATUS_NEW的记录了,自然返回空。 - 打印
qs有结果:打印的时候会遍历qs的内存缓存,那些对象还在内存里呢,所以能显示出来。 first()返回None:大概率是你在修改后,用原来的qs做了链式操作(比如加过滤)生成了新QuerySet,新QuerySet查数据库是空的;或者是你误以为缓存里的对象还是符合原条件,但其实它们的status已经被改了。
再看第二个外键关联的例子
这段代码的问题本质一样,只是多了个外键:
for bank_transfer in qs.filter(purpose__status='pending_completed'): bank_transfer.purpose.status = 'completed' bank_transfer.purpose.save()
- 遍历的时候,QuerySet查数据库拉了符合
purpose__status='pending_completed'的BankTransfer,存在缓存里。 - 你修改了关联的
Purpose对象并保存,数据库里的Purpose状态变了,但缓存里的BankTransfer对象关联的还是旧的Purpose内存对象。 - 之后再过滤的话,新QuerySet查数据库找不到符合条件的记录,返回空;但打印原
qs还是会显示缓存里的旧对象。
解决办法来了,按需选!
方法1:别复用旧QuerySet,重新查!
最简单的办法:修改完数据后,不要用原来的qs变量,重新写一遍过滤语句,强制查数据库:
# 修改完数据后,重新创建QuerySet new_qs = queryset.filter(status=models.BankTransfer.STATUS_NEW) print(new_qs.first()) # 这会返回数据库里的真实最新结果
方法2:手动清空缓存,让它重新查
如果你非得用原来的qs,可以手动清空它的缓存,这样下次访问就会重新跑数据库:
qs = queryset.filter(status=models.BankTransfer.STATUS_NEW) for bank_transfer in qs: bank_transfer.status = models.BankTransfer.STATUS_APPROVED bank_transfer.save() # 清空缓存(直接操作内部属性,虽然有点“野”但管用) qs._result_cache = None # 现在访问qs会重新查数据库 print(qs.first()) # 会返回None,因为数据库里确实没STATUS_NEW的记录了
方法3:用批量更新,从根源避免问题(强推!)
如果你的修改是统一的(比如把所有STATUS_NEW改成STATUS_APPROVED),完全没必要循环一个个改!用update()直接在数据库层面批量更新,既快又不会有缓存问题:
queryset.filter(status=models.BankTransfer.STATUS_NEW).update(status=models.BankTransfer.STATUS_APPROVED)
这种方式根本不会把对象加载到内存,直接执行SQL更新,完美避开缓存陷阱。
方法4:关联对象修改后,手动刷新
如果是外键关联的场景,必须循环修改的话,记得修改后刷新对象,或者重新查:
target_qs = qs.filter(purpose__status='pending_completed') for bank_transfer in target_qs: bank_transfer.purpose.status = 'completed' bank_transfer.purpose.save() # 刷新关联的Purpose对象,获取最新数据 bank_transfer.purpose.refresh_from_db() # 重新查询获取最新结果 updated_qs = qs.filter(purpose__status='pending_completed')
最后总结一下
记住这句话:QuerySet的缓存是内存里的旧快照,和数据库里的数据随时可能不同步。修改数据后,要么重新创建QuerySet查数据库,要么清空缓存,要么用批量更新绕开内存缓存——怎么方便怎么来~
内容的提问来源于stack exchange,提问作者Flupper
相关产品推荐
相关产品推荐

