用于迭代嵌套结果的 Django 查询集预取优化

Posted

技术标签:

【中文标题】用于迭代嵌套结果的 Django 查询集预取优化【英文标题】:Django Queryset Prefetch Otimization for iterating over nested results 【发布时间】:2016-07-21 07:32:22 【问题描述】:

考虑到我想要获取嵌套关系,我正在寻找一种通过提高数据库访问性能来优化 Django 中查询集结果处理的方法。

例如,我做了这样的结构:

class Movie(models.Model):
    name = models.CharField(max_length=50)

class Ticket(models.Model):
    code = models.CharField(max_length=255, blank=True, unique=True)
    movie = models.ForeignKey(Movie, related_name='tickets')

class Buyer(models.Model):
    name = models.CharField(max_length=50)

class Purchase(models.Model):
    tickets = models.ManyToManyField(Ticket, related_name='purchases')
    buyer = models.ForeignKey(Buyer, related_name='purchases')

假设我有一个电影查询集:

movies = Movie.objects.all().prefetch_related('tickets__purchases__buyer')

如果我想在这个 qs 中检索每部电影的所有买家,我可以这样做:

for movie in movies:
    buyers = Buyer.objects.filter(purchases__tickets__in=movie.tickets.all()).distinct()

但是在这种方法中,它会为每个迭代的电影再次访问数据库。要解决此问题,我执行以下操作:

def get_movie_buyers(movie):
    buyers = set()
    for ticket in movie.tickets.all():
        for purchase in ticket.purchases.all():
            if purchase.buyer:
                buyers.add(purchase.buyer)
    return buyers
for movie in movies:
    buyers = get_movie_buyers(movie)
    # do something with the buyers

这种方式只访问数据库一次,因为我之前使用过 prefetch_related,但我认为这不是一个好方法,因为我必须迭代许多嵌套循环,这会增加内存过载。

我认为有更好的方法,但我仍然没有找到“正确”的方法,希望有人能指导我。

更新

正如alasdair 提到的,要使用 Prefetch 对象,但我已经尝试使用以下内容:

movies = Movie.objects.prefetch_related(
    Prefetch(lookup='tickets__purchases__buyer',
             to_attr='buyers')
).all()
for movie in movies:
    print movie.buyers

这给了我以下错误: 'Movie' object has no attribute 'buyers'

【问题讨论】:

我认为您可以使用Prefetch() 对象来做到这一点。 我尝试过使用 Prefetch() 对象:Prefetch(lookup='tickets__purchases__buyer', to_attr='buyers'),但我不知道如何构建 queryset 参数,因为询问的买家与自己的电影有关。你能给我举个例子吗? 对不起,我不知道具体如何处理你的情况,我留下了评论,因为我认为这可能。 尝试在prefetch_related 调用之后添加.all() 即使使用.all() 我也会得到 AttributeError 【参考方案1】:

看起来太难的原因是购买和门票之间的多对多关系。

这种关系允许在多次购买中出现同一张票。但实际数据并非如此,因为一张票只能购买一次。 如果您删除此 ManyToMany 字段并在 Ticket to purchase 中添加 ForeignKey 字段,则可以简化查询

class Ticket(models.Model):
    code = models.CharField(max_length=255, blank=True, unique=True)
    movie = models.ForeignKey(Movie, related_name='tickets')
    purchase = models.ForeignKey(Purchase, null=True, blank=True)

那么查询可以简化如下。

movies = Movie.objects.all().prefetch_related('tickets__purchase__buyer')
for movie in movies:
    print(set(ticket.purchase.buyer for ticket in movie.tickets if ticket.purchase))

这肯定会在创建购买时增加额外的复杂性,因为您需要使用 purchase_id 更新票证对象。

您需要根据两个操作的频率来确定在何处保持复杂性

【讨论】:

以上是关于用于迭代嵌套结果的 Django 查询集预取优化的主要内容,如果未能解决你的问题,请参考以下文章

Django 查询集。如何只预取唯一的?

Django模板:获取嵌套循环的总迭代次数

Django 1.9.5:偶尔出现反向查找字段的 FieldError

Django预取相关并存在

Django 模板 - 嵌套字典迭代

Django 嵌套循环的迭代总数