Django 大量将模型对象转换为字典导致服务器超时

Django convert model objects to dictionary in large volumes results in server timeout

我一直遇到 Django 服务器需要很长时间才能 return 响应的问题。当 Heroku 中的 运行 gunicorn 我超时,所以无法收到响应。如果我在本地 运行,需要一段时间,但一段时间后它会正确显示该站点。

基本上我有两个模型:

class Entry(models.Model):
    #Some stuff charFields, foreignKey, TextField and ManyToMany fields
    #Eg of m2m field:
    tags = models.ManyToManyField(Tag, blank=True)

    def getTags(self):
        ret = []
        for tag in self.tags.all():
            ret.append(getattr(tag, "name"))
        return ret

    def convertToDict(self):
        #for the many to many field I run the getter
        return {'id': self.id, 'tags' : self.getTags(), ... all the other fields ... }

class EntryState(models.Model):
    entry = models.ForeignKey(Entry, on_delete=models.CASCADE)
    def convertToDict(self):
        temp = self.entry.convertToDict()
        temp['field1'] = self.field1
        temp['field2'] = self.field1 + self.field3
        return temp

那我有一个看法:

def myView(request):
    entries = list(EntryState.objects.filter(field1 < 10))

    dictData = {'entries' : []}
    for entry in entries:
        dictData['entries'].append(entry.convertToDict())

    context = {'dictData': dictData}
    return render(request, 'my_template.html', context)

这样做的方式是,dictData 包含必须在站点中显示的所有条目的信息。然后,这个变量被加载到一个 javascript 中,它将决定如何显示信息。

当有很多带有 field1 < 10 的条目时,视图会卡在 for 循环中。

我只是想知道,可以做些什么来改进这种方法。有了 +600 个条目,它已经花费了很长时间,足以让 gunicorn 超时。

如果相关,我使用的是 Django 4.0.3 和 Python3。 Alpine.js 在前端使用数据来呈现网站。

您正在尝试呈现包含大量数据的模板。

在这种情况下,我认为您应该考虑将发送到模板和条目中的数据拆分为两种不同的请求类型。这意味着您发送模板,然后您的 alpine.js 一个一个地加载条目。 (编辑:我的意思是每包 50 件左右,请不要对每件作品都提出要求...)

为此,您可以使用现成的 REST Framework Application for django 或部署您自己的解决方案。

当然这样用户在加载模板后将看不到任何数据。相反,他们将不得不等待第二个请求。 对此的高级解决方案是仅呈现用户将看到的前 100 个左右的条目,并使用新的 REST API 来处理剩余的数据。您可以使用延迟加载 alpine.js 插件 Intersect,它对您的整体性能也有帮助!

乍一看,您的主要问题很可能是您为每个 EntryState 实例访问数据库两次。

convertToDict 方法使用 FK entry 并且对于每个条目,您还获取 M2M tags。解决方案是优化查询。

首先,让我们找出问题所在。 当您将此代码放在视图末尾之前时,您将在控制台中看到您访问数据库的次数。

def myView(request):
    ....
    from django.db import connection; qq=connection.queries; print(f'QUERY: {len(qq)}')
    return render(request, 'my_template.html', context)

现在,尝试改进视图中的查询。

query = EntryState.objects.filter(field1 < 10)  # Assuming this is a valid query that uses something like `field1__lt=10`

您可以使用 select_related 作为条目外键,以便在单个查询中检索它(将 N 次匹配保存到数据库)

query = EntryState.objects.filter(field1__lt=10).select_related('entry')

您还可以添加相关的预取,以便一次性检索所有条目的 tags

query = EntryState.objects.filter(field1__lt=10).select_related('entry').prefetch_related('entry__tags_set')

每次更改后,您都可以看到访问数据库的次数,这样您就可以看到问题得到解决。您可以再次检查数据库命中数以确保其已优化。

有关详细信息,请阅读查询优化,select_relatedprefetch_related

您还可以使用一些应用程序来监控视图的性能:django-silk, django-debug-toolbar

来源:https://docs.djangoproject.com/en/4.0/ref/models/querysets/#select-related