我的 API 中有一个模型,它有一个包含数万条记录的表的外键。当我在可浏览 UI 中浏览到该模型的详细信息页面时,页面加载需要很长时间,因为它试图用 PUT 命令的 HTML 表单的数万个条目填充外键下拉列表。
有没有办法解决这个问题?我认为我最好的解决方案是让可浏览的 UI 不显示此字段,从而防止加载缓慢。人们仍然可以直接通过实际的 PUT API 请求更新字段。
谢谢。
看看使用自动完成小部件,或者下拉到使用哑文本字段小部件。
自动完成文档在这里:http://www.django-rest-framework.org/topics/browsable-api/#autocomplete
请注意,您可以禁用 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',
),
}
这将加快速度,您仍然可以从可浏览的UI发布。
您可以强制使用文本输入,方法很简单:
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文档的这一部分返回的结果。
对于没有明显问题来说,这是一个很好的问题。你在学习 Django 时陷入的错误假设,以及在阅读官方文档时与之相关的插件 DRF,将创建一个根本不真实的概念模型。我在这里谈论的是,Django 明确地为关系数据库设计,并没有让它开箱即用!
问题
在ORM世界中查询包含关系(例如一对多(的模型时,Django/DRF运行缓慢的原因被称为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 个查询中获取所有数据。
相关读物
- 解决方案 DRF 1
- 解决方案 DRF 2
- 解决方案姜戈
- N+1 链路 1
- N+1 链路 2
DRF 文档中有一节给出了以下建议:
author = serializers.HyperlinkedRelatedField(
queryset=User.objects.all(),
style={'base_template': 'input.html'}
)
如果文本字段太简单,正如@Chozabu上面在我写这个答案之前很久的评论中提到的,他们建议在 HTML 模板中手动添加自动完成:
另一种但更复杂的选项是将输入替换为自动完成小部件,该小部件仅根据需要加载和呈现可用选项的子集。如果需要执行此操作,则需要自己执行一些工作来构建自定义自动完成 HTML 模板。
自动完成小部件有各种各样的包,例如 django-autocomplete-light,你可能想要参考。请注意,您将无法简单地将这些组件作为标准小部件包含在内,而是需要显式编写 HTML 模板。这是因为 REST 框架 3.0 不再支持 widget 关键字参数,因为它现在使用模板化的 HTML 生成。
这显然是 DRF 可浏览 API 的已知问题。
如果你使用 DjangoFilterBackend 作为默认的 DRF 过滤器后端,那么你很幸运。 很容易在可浏览 API 模板中禁用生成这些缓慢构建的筛选器 - 适用于所有视图,或仅针对单个视图。
只是像这样子类 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,)