五個常見的Django錯誤彙總!

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):

不使用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" = '[email protected]',
"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,並向他們的電子郵件提交更新,將其更新從[email protected]更改爲[email protected]。電子郵件更新提交到數據庫後,外部API響應,並且celery worker將用戶的is_active更新爲False。

在這種情況下,celery worker會覆蓋電子郵件更新,因爲該工作者會在提交電子郵件更新之前將整個用戶對象加載到內存中。當celery worker將用戶加載到內存中時,該用戶的電子郵件爲[email protected]。該電子郵件將保留在內存中,直到外部API響應並覆蓋電子郵件更新爲止。最後,代表數據庫內部用戶的行包含舊電子郵件地址[email protected]和is_active = False。讓我們更改代碼以防止出現這種情況。

if user_status == "inactive":
    user.is_active = False
    user.save(update_fields=["is_active"])

如果以前的情況是使用更新的代碼發生的,那麼在celery worker更新is_active之後,用戶的電子郵件仍爲[email protected],因爲該更新僅寫入is_active字段。僅在極少數情況下(例如創建新對象),才應調用不帶update_fields的保存。雖然可以通過不調用簡單的save方法在代碼庫中解決此問題,但第三方Django程序包可能包含此問題。例如,Django REST Framework不在PATCH請求上使用update_fields。Django REST Framework是我喜歡使用的出色軟件包,但無法解決此問題。將第三方軟件包添加到Django項目時,請記住這一點。

寫在最後
我已經多次犯了所有這些錯誤。我希望這篇文章能揭示日常代碼中潛在的錯誤,並防止這些錯誤發生。我喜歡使用Django,而且我認爲這是構建Web應用程序的非常好的框架。但是,在任何大型框架下,複雜性都會變得模糊不清,都可能會犯錯誤,該趟的坑一個也不會少。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章