跳转到主要内容

Django应用程序中查询执行的显式控制。

项目描述

django-zen-queries

Build Status pypi release

让您控制代码中哪些部分可以运行查询,哪些不可以。

在Python 3.6 - 3.9上测试了Django 2.2 - 3.1。

动机

明确优于隐晦

(《Python之禅》)

Django ORM的最强之处也是其最大弱点。通过让开发者从必须考虑何时执行数据库查询中解脱出来,ORM鼓励开发者 不思考何时执行数据库查询!这对于快速开发周转来说往往有很大的好处,但除了最简单系统外,都可能带来重大的性能影响。

Django的ORM使查询 隐晦。Python之禅告诉我们,明确优于隐晦,所以让我们明确哪些代码部分允许运行查询,哪些不允许。

查看 这篇博客文章 了解更多背景信息。

示例

想象一个披萨餐厅网站,有如下模型

class Topping(models.Model):
    name = models.CharField(max_length=100)


class Pizza(models.Model):
    name = models.CharField(max_length=100)
    toppings = models.ManyToManyField(Topping)

这是菜单视图

def menu(request):
    pizzas = Pizza.objects.all()
    context = {'pizzas': pizzas}
    return render(request, 'pizzas/menu.html', context)

最后,这是模板

<h1>Pizza Menu</h1>

<ul>
{% for pizza in pizzas %}
  <li>{{ pizza.name }}</li>
{% endfor %}
</ul>

这里运行了多少个查询?答案很简单:只有一个!由 Pizza.objects.all() 发出的查询就足以获取显示在菜单上的信息。

现在:想象客户要求菜单上的每个披萨都要包含披萨上配料数量的计数。很简单!只需更改模板

<h1>Pizza Menu</h1>

<ul>
{% for pizza in pizzas %}
  <li>{{ pizza.name }} ({{ pizza.toppings.count }})</li>
{% endfor %}
</ul>

但当前运行了多少个查询呢?嗯,这是一个经典的n查询问题。我们现在有一个查询来获取所有的披萨,然后对于每个披萨再有一个查询来获取配料数量。披萨越多,应用程序运行越慢。 我们可能只有在网站投入生产后才会发现这个问题

如果你在阅读Django性能教程,接下来的步骤会告诉你如何解决这个问题(.annotateCount等)。但这不是重点。上面的例子只是说明了代码库中不同部分的代码,在抽象层次不同,甚至在较大的项目中可能由不同的开发者负责,它们如何相互作用导致性能低下。面向对象设计鼓励黑盒实现隐藏,但如果你的目标是构建高性能的Web应用程序,隐藏查询执行的点是最糟糕的事情。那么我们如何在不破坏所有抽象的情况下解决这个问题呢?

这里有两个技巧

  1. 防止开发者在不意识到的情况下意外运行查询。
  2. 鼓励设计代码,将获取数据渲染数据分开。

这个包提供了三件非常简单的事情

  1. 一个上下文管理器,允许开发者明确查询运行的位置。
  2. 一个工具,使查询集不那么懒惰。
  3. 一些工具,使得使用Django模板和Django REST框架序列化器与上下文管理器结合使用变得简单。

要绝对清楚:这个包没有提供任何实际改善你的查询模式的工具。它只是告诉你何时需要这样做!

说明

为了演示如何使用django-zen-queries,让我们回到我们的例子。我们希望确保对模板的更改不会触发查询。所以,我们按照以下方式更改我们的视图

def menu(request):
    pizzas = Pizza.objects.all()
    context = {'pizzas': pizzas}
    with queries_disabled():
        return render(request, 'pizzas/menu.html', context)

这里的queries_disabled上下文管理器做了一件事:它阻止任何代码在它里面运行数据库查询。一点都没有。如果它们尝试运行一个查询,应用程序将引发一个QueriesDisabledError异常并崩溃。

这几乎已经足够给我们我们需要的,但还不够。上面的代码将始终引发一个QueriesDisabledError,因为查询集(Pizza.objects.all())是懒惰的。数据库查询实际上只有在查询集迭代时才会运行——这发生在模板中!所以,django-zen-queries提供了一个小小的辅助函数,fetch,它强制评估查询集

def menu(request):
    pizzas = Pizza.objects.all()
    context = {'pizzas': fetch(pizzas)}
    with queries_disabled():
        return render(request, 'pizzas/menu.html', context)

现在我们正好需要这样的东西:当开发人员来到模板中并添加{{ pizza.toppings.count }}时,它根本不会工作。他们被迫找出如何使用annotateCount来获取他们所需的数据,而不是在将来某个时候,当客户抱怨网站越来越慢时!

装饰器

您还可以将queries_disabled用作装饰器,以禁止函数或方法与数据库交互

@queries_disabled()
def validate_xyz(pizzas):
    ...

这也与Django的method_decorator实用工具一起工作。

额外工具

除了上下文管理器外,该包还提供了一些工具,使得在常见情况下使用更加容易

渲染快捷方式

如果您正在使用Django的render快捷方式(如上面的例子),为了避免在每一个视图中都添加上下文管理器,您可以将导入从from django.shortcuts import render更改为from zen_queries import render。该文件中的所有视图将自动禁止在模板渲染期间运行查询。

TemplateResponse子类

TemplateResponse(以及SimpleTemplateResponse)对象是懒加载的,这意味着模板渲染是在Django栈的“出口”过程中发生的。zen_queries.TemplateResponsezen_queries.SimpleTemplateResponse是这些对象的子类,并在render方法上应用了queries_disabled

您可以通过在视图中将response_class属性设置为zen_queries.TemplateResponse,告诉Django的基于类的视图使用这些子类而不是默认的TemplateResponse

Django REST框架序列化和视图混合

Django REST框架序列化器是意外查询的另一个主要来源。向序列化器中添加一个字段(可能在嵌套序列化器的树中很深)可以非常容易地导致您的应用程序突然开始发出数百个查询。zen_queries.rest_framework.QueriesDisabledSerializerMixin可以添加到任何序列化器中,以便将queries_disabled应用于.data属性,这意味着序列化阶段不允许执行任何查询。

您可以通过以下方式将此混合添加到现有的序列化器实例中:serializer = disable_serializer_queries(serializer)

如果您正在使用REST框架通用视图,也可以添加一个视图混合,zen_queries.rest_framework.QueriesDisabledViewMixin,该混合覆盖了get_serializer以将QueriesDisabledSerializerMixin混合到现有的序列化器中。这很有用,因为您可能希望在多个视图中使用相同的序列化器类,但在某些上下文中禁用查询,例如在列表视图中。请记住,Python MRO是左到右的,因此混合必须放在任何实现get_serializer的基类之前。视图混合仅在GET请求上禁用查询,因此可以安全地与ListCreateAPIView和类似视图一起使用。

逃生门

如果您绝对肯定不能避免在执行在queries_disabled块下的代码库的一部分时运行查询,还有一个名为queries_dangerously_enabled的上下文管理器,它允许您暂时重新启用数据库查询。

模板标签

Django模板系统提供了块标签,允许您直接在模板中启用或禁用查询执行。

重要提示:为了使用模板库,您必须将"zen_queries"添加到您的INSTALLED_APPS设置中。然后,在模板顶部使用{% load zen_queries %}来加载标签库。

{% queries_disabled}标签在您希望将django-zen-queries模式应用于通过覆盖模板提供定制的第三方库(如Django管理后台)时非常有用。

{% load zen_queries %}

{% queries_disabled %}
<ul>
{% for pizza in pizzas %}
  <li>{{ pizza.name }}</li>
{% endfor %}
</ul>
{% end_queries_disabled %}

{% queries_dangerously_enabled %}标签在您使用render快捷方式或TemplateResponse子类(见上文)时非常有用,但希望允许模板的特定部分执行查询。这应该谨慎使用,并且您应该仅包装模板中最小可能的部分:需要执行查询的确切行或几行。

{% load zen_queries %}

{% queries_dangerously_enabled %}
There are {{ pizzas.count }} pizzas.
{% end_queries_dangerously_enabled %}

权限陷阱

通过模板变量{{ perms }}访问权限在模板渲染时可能会引起查询。幸运的是,Django的权限检查被ModelBackend缓存,这可以通过在视图中调用request.user.get_all_permissions()来预先填充,在渲染模板之前。

它是如何工作的?

它使用Django 2.0中引入的数据库监控功能

安装

从PyPI安装

pip install django-zen-queries

行为准则

有关向此存储库贡献时的行为准则指南,请参阅https://www.dabapps.com/open-source/code-of-conduct/

项目详情


下载文件

下载适合您平台的文件。如果您不确定该选择哪一个,请了解有关安装包的更多信息。

源代码分发

django-zen-queries-2.1.0.tar.gz (10.4 kB 查看哈希值)

上传时间 源代码

构建分发

django_zen_queries-2.1.0-py3-none-any.whl (12.4 kB 查看哈希值)

上传时间 Python 3