Django преобразование объектов модели в словарь в больших объемах приводит к таймауту сервера

У меня возникла проблема, когда сервер Django требует вечность для возврата ответа. При запуске с gunicorn в Heroku я получаю таймаут, поэтому не могу получить ответ. Если я запускаю локально, это занимает некоторое время, но через некоторое время он правильно показывает сайт.

В основном у меня есть две модели:

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, который решает, как отобразить информацию.

Представление застревает в цикле for, когда есть много записей с field1 < 10.

Мне просто интересно, что можно сделать, чтобы улучшить этот подход. С +600 записями это уже занимает вечность, достаточно, чтобы gunicorn прервалось.

В случае, если это имеет значение, я использую Django 4.0.3 и Python3. Данные используются во фронтенде Alpine.js для рендеринга сайта.

Вы пытаетесь отобразить шаблон, содержащий огромное количество данных.

В этом случае, я думаю, вам следует рассмотреть возможность разделения данных, которые вы отправляете, на шаблон и записи, как два разных типа запроса. То есть вы отправляете шаблон, а затем ваш alpine.js загружает записи по одной. (Edit: Я имею в виду пакеты по 50 или около того, пожалуйста, не делайте запрос для каждой записи...)

Для этого вы можете использовать либо готовое приложение REST Framework Application для 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 для Entry FK, чтобы он был получен в рамках одного запроса (экономия 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_related и prefetch_related.

Вы также можете использовать некоторые приложения для мониторинга производительности ваших представлений: django-silk, django-debug-toolbar

Источник: https://docs.djangoproject.com/en/4.0/ref/models/querysets/#select-related

Вернуться на верх