五个常见的Django错误
共 7678字,需浏览 16分钟
·
2021-05-02 10:10
Django是用于构建Web应用程序的非常好的框架,但在我们还不太熟悉的情况下开发中可能由于某些的疏忽会而带来一些细微的错误,本篇目的是供我总结的一些内容,供参考,总结下来也方便自己后续避免犯错,在本文中,我们将开发一个示例Django应用程序,该应用程序可以处理各种组织的员工管理。
示例代码:
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import models
User = get_user_model()
class Organization(models.Model):
name = models.CharField(max_length=100)
datetime_created = models.DateTimeField(auto_now_add=True, editable=False)
is_active = models.BooleanField(default=True)
class Employee(models.Model):
user = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="employees"
)
organization = models.ForeignKey(
Organization, on_delete=models.CASCADE, related_name="employees"
)
is_currently_employed = models.BooleanField(default=True)
reference_id = models.CharField(null=True, blank=True, max_length=255)
last_clock_in = models.DateTimeField(null=True, blank=True)
datetime_created = models.DateTimeField(auto_now_add=True, editable=False)
def clean(self):
try:
if self.last_clock_in < self.datetime_created:
raise ValidationError(
"Last clock in must occur after the employee entered"
" the system."
)
except TypeError:
# Raises TypeError if there is no last_clock_in because
# you cant compare None to datetime
pass
不使用select_related和prefetch_related
假设我们编写了一些遍历每个组织员工的代码。
for org in Organization.objects.filter(is_active=True):
for emp in org.employees.all():
if emp.is_currently_employed:
do_something(org, emp)
此循环导致查询数据库中的每个员工。这可能会导致成千上万的查询,这会减慢我们的应用程序的速度。但是,如果我们添加与组织查询相关的prefetch_related,我们将使查询量最小化。
for org in Organization.objects.filter(is_active=True).prefetch_related( "employees"):
添加这些方法无需大量工作即可大大提高性能,但是添加它们很容易忘记。对于ForeignKey或OneToOneField,请使用select_related。对于反向的ForeignKey或ManyToManyField,请使用prefetch_related。我们可以通过从employee表开始并使用数据库过滤结果来提高效率。由于函数do_something使用员工的组织,因此我们仍然需要添加select_related。如果我们不这样做,则循环可能导致对组织表的成千上万次查询。
for emp in Employee.objects.filter(
organization__is_active=True, is_currently_employed=True
).select_related("organization"):
do_something(emp.organization, emp)
向CharField或TextField添加null
Django的文档建议不要向CharField添加null = True。查看我们的示例代码,该员工的参考ID包含null = True。在示例应用程序中,我们可以选择与客户的员工跟踪系统集成,并使用reference_id作为集成系统的ID。
reference_id = models.CharField(null=True, blank=True, max_length=255)
添加null = True表示该字段具有两个“无数据”值,即null和空字符串。按照惯例,Django使用空字符串表示不包含任何数据。通过将null作为“无数据”值,我们可以引入一些细微的错误。假设我们需要编写一些代码来从客户系统中获取数据。
if employee.reference_id is not None:
fetch_employee_record(employee)
理想情况下,可以使用if employee.reference_id:编写if语句来处理任何“无数据”值,但是我发现实际上并不会发生这种情况。由于reference_id可以为null或空字符串,因此我们在此处创建了一个错误,如果reference_id为空字符串,系统将尝试获取员工记录。显然,这是行不通的,并且会导致我们的系统出现错误。根据Django的文档,将null = True添加到CharField存在一个例外。当需要同时将blank = True和unique = True添加到CharField时,则需要null = True。
使用order_by或last降序或升序
Django的order_by默认为升序。通过在关键字前面添加-,可以指示Django提供降序排列。让我们看一个例子。
oldest_organization_first = Organization.objects.order_by("datetime_created")
newest_organization_first = Organization.objects.order_by("-datetime_created")
在datetime_created前面加上减号后,Django首先为我们提供了最新的组织。相反,没有减号,我们首先获得最早的组织。错误地使用默认的升序会导致非常细微的错误。Django查询集还带有最新的,它根据传递的关键字字段为我们提供了表中的最新对象。最新的方法默认为降序,而order_by默认为升序。
oldest_organization_first = Organization.objects.latest("-datetime_created")
newest_organization_first = Organization.objects.latest("datetime_created")
在多个项目中,由于last和order_by之间的默认值不同,导致引入了一些错误。请谨慎编写order_by和last查询。让我们看看使用last和order_by进行的等效查询。
>>> oldest_org = Organization.objects.order_by("datetime_created")[:1][0]
>>> oldest_other_org = Organization.objects.latest("-datetime_created")
>>> oldest_org == oldest_other_org
True
>>> newest_org = Organization.objects.order_by("-datetime_created")[:1][0]
>>> newest_other_org = Organization.objects.latest("datetime_created")
>>> newest_org == newest_other_org
True
忘记保存时调用clean方法
根据Django的文档,模型的save方法不会自动调用模型验证方法,例如clean,validate_unique和clean_fields。在我们的示例代码中,员工模型包含一个clean的方法,该方法指出last_clock_in不应在员工进入系统之前发生。
def clean(self):
try:
if self.last_clock_in < self.datetime_created:
raise ValidationError(
"Last clock in must occur after the employee entered"
" the system."
)
except TypeError:
# Raises TypeError if there is no last_clock_in because
# you cant compare None to datetime
pass
假设我们有一个视图可以更新员工的last_clock_in时间,作为该视图的一部分,我们可以通过调用save来更新员工。
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_http_methods
from example_project.helpers import parse_request
from example_project.models import Employee
@require_http_methods(["POST"])
def update_employee_last_clock_in(request, employee_pk):
clock_in_datetime = parse_request(request)
employee = get_object_or_404(Employee, pk=employee_pk)
employee.last_clock_in = clock_in_datetime
employee.save()
return HttpResponse(status=200)
在我们的示例视图中,我们调用save而不调用clean或full_clean,这意味着传递到我们视图中的clock_in_datetime可能发生在员工创建datetime__date之前,并且仍保存到数据库中。这导致无效数据进入我们的数据库。让我们修复我们的错误。
employee.last_clock_in = clock_in_datetime
employee.full_clean()
employee.save()
现在,如果clock_in_datetime在员工的datetime_created之前,full_clean将引发ValidationError,以防止无效数据进入我们的数据库。
保存时不包括update_fields
Django Model的save方法包括一个名为update_fields的关键字参数。在针对Django的典型生产环境中,人们使用gunicorn在同一台计算机上运行多个Django服务器进程,并使用celery运行后台进程。当调用不带update_fields的保存时,整个模型将使用内存中的值进行更新。让我们看一下实际的SQL来说明。
>>> user = User.objects.get(id=1)
>>> user.first_name = "Steven"
>>> user.save()
UPDATE "users_user"
SET "password" = 'some_hash',
"last_login" = '2021-02-25T22:43:41.033881+00:00'::timestamptz,
"is_superuser" = false,
"username" = 'stevenapate',
"first_name" = 'Steven',
"last_name" = '',
"email" = 'steven@laac.dev',
"is_staff" = false,
"is_active" = true,
"date_joined" = '2021-02-19T21:08:50.885795+00:00'::timestamptz,
WHERE "users_user"."id" = 1
>>> user.first_name = "NotSteven"
>>> user.save(update_fields=["first_name"])
UPDATE "users_user"
SET "first_name" = 'NotSteven'
WHERE "users_user"."id" = 1
一次调用不带update_fields的保存将导致保存用户模型上的每个字段。使用update_fields时,仅first_name更新。在频繁写入的生产环境中,在没有update_fields的情况下调用save可能导致争用情况。假设我们有两个进程正在运行,一个运行我们的Django服务器的gunicorn工人和一个celery worker。按照设定的时间表,celery worker将查询外部API,并可能更新用户的is_active。
from celery import task
from django.contrib.auth import get_user_model
from example_project.external_api import get_user_status
User = get_user_model()
@task
def update_user_status(user_pk):
user = User.objects.get(pk=user_pk)
user_status = get_user_status(user)
if user_status == "inactive":
user.is_active = False
user.save()
celery worker启动任务,将整个用户对象加载到内存中,并查询外部API,但是外部API花费的时间比预期的长。当celery worker等待外部API时,同一用户连接到我们的gunicorn worker,并向他们的电子邮件提交更新,将其更新从steven@laac.dev更改为steven@stevenapate.com。电子邮件更新提交到数据库后,外部API响应,并且celery worker将用户的is_active更新为False。
在这种情况下,celery worker会覆盖电子邮件更新,因为该工作者会在提交电子邮件更新之前将整个用户对象加载到内存中。当celery worker将用户加载到内存中时,该用户的电子邮件为steven@laac.dev。该电子邮件将保留在内存中,直到外部API响应并覆盖电子邮件更新为止。最后,代表数据库内部用户的行包含旧电子邮件地址steven@laac.dev和is_active = False。让我们更改代码以防止出现这种情况。
if user_status == "inactive":
user.is_active = False
user.save(update_fields=["is_active"])
如果以前的情况是使用更新的代码发生的,那么在celery worker更新is_active之后,用户的电子邮件仍为steven@stevenapate.com,因为该更新仅写入is_active字段。仅在极少数情况下(例如创建新对象),才应调用不带update_fields的保存。虽然可以通过不调用简单的save方法在代码库中解决此问题,但第三方Django程序包可能包含此问题。例如,Django REST Framework不在PATCH请求上使用update_fields。Django REST Framework是我喜欢使用的出色软件包,但无法解决此问题。将第三方软件包添加到Django项目时,请记住这一点。
写在最后
我已经多次犯了所有这些错误。我希望这篇文章能揭示日常代码中潜在的错误,并防止这些错误发生。我喜欢使用Django,而且我认为这是构建Web应用程序的非常好的框架。但是,在任何大型框架下,复杂性都会变得模糊不清,都可能会犯错误,该趟的坑一个也不会少。
(版权归原作者所有,侵删)
点击下方“阅读原文”查看更多