Django REST Framework: медленный пользовательский интерфейс из-за большой связанной таблицы

В моем API есть модель, которая имеет внешний ключ к таблице с десятками тысяч записей. Когда я просматриваю страницу сведений об этой модели в пользовательском интерфейсе с возможностью просмотра, загрузка страницы длится вечно, потому что она пытается заполнить раскрывающийся список внешнего ключа десятками тысяч записей для формы HTML для команды PUT.

Есть ли способ обойти это? Я думаю, что моим лучшим решением было бы, чтобы пользовательский интерфейс, отображаемый в браузере, не отображал это поле и таким образом предотвращал медленную загрузку. Люди могут по-прежнему обновлять поле с помощью фактического запроса API PUT напрямую.

Благодарю.

6 ответов

Решение

Взгляните на использование виджета автозаполнения или выберите простой виджет текстового поля.

Документы по автозаполнению здесь: http://www.django-rest-framework.org/topics/browsable-api/

Обратите внимание, что вы можете отключить HTML-форму и сохранить необработанные данные в формате json:

class BrowsableAPIRendererWithoutForms(BrowsableAPIRenderer):
    """Renders the browsable api, but excludes the forms."""
    def get_rendered_html_form(self, data, view, method, request):
        return None

и в settings.py:

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': (
        'rest_framework.renderers.JSONRenderer',
        'application.api.renderers.BrowsableAPIRendererWithoutForms',
    ),
}

это ускорит процесс, и вы все равно сможете отправлять сообщения из пользовательского интерфейса.

Вы можете принудительно использовать TextInput с помощью простого:

from django.forms import widgets
...
class YourSerializer(serializers.ModelSerializer):
    param = serializers.PrimaryKeyRelatedField(
        widget=widgets.TextInput
    )

Или после правильной конфигурации autocomplete_light:

import autocomplete_light
...
class YourSerializer(serializers.ModelSerializer):
    paramOne = serializers.PrimaryKeyRelatedField(
        widget=autocomplete_light.ChoiceWidget('RelatedModelAutocomplete')
    )
    paramMany = serializers.PrimaryKeyRelatedField(
        widget=autocomplete_light.MultipleChoiceWidget('RelatedModelAutocomplete')
    )

Чтобы отфильтровать результаты, которые возвращает autocomplete_light, эта часть документации.

Очевидно, это известная проблема с DRF BrowsableAPI.

Если вы используете DjangoFilterBackend в качестве бэкенда фильтра DRF по умолчанию, вам повезло. Создание этих медленных фильтров легко отключить в шаблоне BrowsableAPI — для всех представлений или только для одного представления.

Просто подкласс DjangoFilterBackend вот так:

      class DjangoFilterBackendWithoutForms(DjangoFilterBackend):
    """
    The Browsable API renders very slowly for models with foreign keys to large tables.
    As a workaround, views can swap in this filter backend to skip form rendering.
    """
    def to_html(self, request, queryset, view):
        return None

Затем используйте его в качестве бэкенда фильтра по умолчанию или выберите, для каких представлений вы хотите отключить формы фильтра:

      class MyThingyViewSet(viewsets.ModelViewSet):
    queryset = models.MyThingy.objects.all()
    serializer_class = serializers.MyThingySerializer
    filter_backends = (DjangoFilterBackendWithoutForms,)

В документации DRF есть раздел, в котором дается следующее предложение:

      author = serializers.HyperlinkedRelatedField(
    queryset=User.objects.all(),
    style={'base_template': 'input.html'}
)

Если текстовое поле слишком простое, как упоминал @Chozabu выше в комментарии задолго до того, как я написал этот ответ, они рекомендуют вручную добавить автозаполнение в шаблон HTML:

Альтернативным, но более сложным вариантом может быть замена ввода виджетом автозаполнения, который загружает и отображает только подмножество доступных параметров по мере необходимости. Если вам нужно сделать это, вам нужно будет проделать некоторую работу, чтобы самостоятельно создать собственный HTML-шаблон автозаполнения.

Существует множество пакетов для виджетов автозаполнения, таких как django-autocomplete-light, к которым вы можете обратиться. Обратите внимание, что вы не сможете просто включить эти компоненты в качестве стандартных виджетов, вам нужно будет явно написать HTML-шаблон. Это связано с тем, что платформа REST 3.0 больше не поддерживает аргумент ключевого слова виджета, поскольку теперь она использует создание шаблонов HTML.

Это очень хороший вопрос для ни одной очевидной проблемы. Предположения о неисправности, которые вы принимаете во время изучения Django, и связанные с ним плагины DRF при чтении официальной документации создадут концептуальную модель, которая просто не соответствует действительности. Я говорю здесь о том, что Django, явно разработанный для реляционных баз данных, не делает это быстро из коробки!

проблема

Причина медленного Django/DRF при запросе модели, которая содержит отношения (например, "один ко многим") в мире ORM, известна как проблема N+1 ( N+1, N+1) и особенно заметна, когда ORM использует ленивый загрузка - Django использует ленивую загрузку!!!

пример

Давайте предположим, что у вас есть модель, которая выглядит следующим образом: читатель имеет много книг. Теперь вы хотели бы получить все книги "Заголовок", прочитанные "хардкорным" читателем. В Django вы выполняете это, взаимодействуя с ORM таким образом.

# First Query: Assume this one query returns 100 readers.
> readers = Reader.objects.filter(type='hardcore')

# Constitutive Queries
> titles = [reader.book.title for reader in readers]

Под капотом. Первое утверждение Reader.objects.filter(type='hardcore') создаст один SQL-запрос, который выглядит примерно так. Мы предполагаем, что он вернет 100 записей.

SELECT * FROM "reader" WHERE "reader"."type" = "hardcore";

Далее для каждого читателя [reader.book.title for reader in readers] Вы должны получить связанные книги. Это в SQL будет выглядеть примерно так.

SELECT * FROM "book" WHERE "book"."id" = 1;
SELECT * FROM "book" WHERE "book"."id" = 2;
...
SELECT * FROM "book" WHERE "book"."id" = N;

То, что вы оставили, это 1, выбрать 100 читателей, а N - получить книги, где N - количество книг. Таким образом, всего у вас есть N+1 запросов к базе данных.

Следствием этого поведения является 101 запрос к базе данных, что в конечном итоге приводит к чрезвычайно долгому времени загрузки небольшого объема данных и замедляет работу Django!

Решение

Решение легко, но не очевидно. Следующая официальная документация для Django или DRF не освещает проблему. В конце вы следуете передовым методам и в итоге получаете медленное применение.

Чтобы решить проблему медленной загрузки, вам нужно будет загрузить данные в Django. Обычно это означает использование соответствующего метода prefetch_related() или select_related() для построения SQL INNER JOIN на моделях / таблицах и извлеките все ваши данные всего за 2 запроса вместо 101.

Связанные чтения

Другие вопросы по тегам