五個常見的Django錯誤

Django是用于構(gòu)建Web應(yīng)用程序的非常好的框架,但在我們還不太熟悉的情況下開發(fā)中可能由于某些的疏忽會而帶來一些細微的錯誤,本篇目的是供我總結(jié)的一些內(nèi)容,供參考,總結(jié)下來也方便自己后續(xù)避免犯錯,在本文中,我們將開發(fā)一個示例Django應(yīng)用程序,該應(yīng)用程序可以處理各種組織的員工管理。

示例代碼:
from django.contrib.auth import get_user_modelfrom django.core.exceptions import ValidationErrorfrom django.db import modelsUser = 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 datetimepass
不使用select_related和prefetch_related
假設(shè)我們編寫了一些遍歷每個組織員工的代碼。
for org in Organization.objects.filter(is_active=True):for emp in org.employees.all():if emp.is_currently_employed:do_something(org, emp)
此循環(huán)導(dǎo)致查詢數(shù)據(jù)庫中的每個員工。這可能會導(dǎo)致成千上萬的查詢,這會減慢我們的應(yīng)用程序的速度。但是,如果我們添加與組織查詢相關(guān)的prefetch_related,我們將使查詢量最小化。
for org in Organization.objects.filter(is_active=True).prefetch_related( "employees"):
添加這些方法無需大量工作即可大大提高性能,但是添加它們很容易忘記。對于ForeignKey或OneToOneField,請使用select_related。對于反向的ForeignKey或ManyToManyField,請使用prefetch_related。我們可以通過從employee表開始并使用數(shù)據(jù)庫過濾結(jié)果來提高效率。由于函數(shù)do_something使用員工的組織,因此我們?nèi)匀恍枰砑觭elect_related。如果我們不這樣做,則循環(huán)可能導(dǎo)致對組織表的成千上萬次查詢。
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。在示例應(yīng)用程序中,我們可以選擇與客戶的員工跟蹤系統(tǒng)集成,并使用reference_id作為集成系統(tǒng)的ID。
reference_id = models.CharField(null=True, blank=True, max_length=255)
添加null = True表示該字段具有兩個“無數(shù)據(jù)”值,即null和空字符串。按照慣例,Django使用空字符串表示不包含任何數(shù)據(jù)。通過將null作為“無數(shù)據(jù)”值,我們可以引入一些細微的錯誤。假設(shè)我們需要編寫一些代碼來從客戶系統(tǒng)中獲取數(shù)據(jù)。
if employee.reference_id is not None:fetch_employee_record(employee)
理想情況下,可以使用if employee.reference_id:編寫if語句來處理任何“無數(shù)據(jù)”值,但是我發(fā)現(xiàn)實際上并不會發(fā)生這種情況。由于reference_id可以為null或空字符串,因此我們在此處創(chuàng)建了一個錯誤,如果reference_id為空字符串,系統(tǒng)將嘗試獲取員工記錄。顯然,這是行不通的,并且會導(dǎo)致我們的系統(tǒng)出現(xiàn)錯誤。根據(jù)Django的文檔,將null = True添加到CharField存在一個例外。當需要同時將blank = True和unique = True添加到CharField時,則需要null = True。
使用order_by或last降序或升序
Django的order_by默認為升序。通過在關(guān)鍵字前面添加-,可以指示Django提供降序排列。讓我們看一個例子。
oldest_organization_first = Organization.objects.order_by("datetime_created")newest_organization_first = Organization.objects.order_by("-datetime_created")
在datetime_created前面加上減號后,Django首先為我們提供了最新的組織。相反,沒有減號,我們首先獲得最早的組織。錯誤地使用默認的升序會導(dǎo)致非常細微的錯誤。Django查詢集還帶有最新的,它根據(jù)傳遞的關(guān)鍵字字段為我們提供了表中的最新對象。最新的方法默認為降序,而order_by默認為升序。
oldest_organization_first = Organization.objects.latest("-datetime_created")newest_organization_first = Organization.objects.latest("datetime_created")
在多個項目中,由于last和order_by之間的默認值不同,導(dǎo)致引入了一些錯誤。請謹慎編寫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_orgTrue>>> newest_org = Organization.objects.order_by("-datetime_created")[:1][0]>>> newest_other_org = Organization.objects.latest("datetime_created")>>> newest_org == newest_other_orgTrue
忘記保存時調(diào)用clean方法
根據(jù)Django的文檔,模型的save方法不會自動調(diào)用模型驗證方法,例如clean,validate_unique和clean_fields。在我們的示例代碼中,員工模型包含一個clean的方法,該方法指出last_clock_in不應(yīng)在員工進入系統(tǒng)之前發(fā)生。
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 datetimepass
假設(shè)我們有一個視圖可以更新員工的last_clock_in時間,作為該視圖的一部分,我們可以通過調(diào)用save來更新員工。
from django.http import HttpResponsefrom django.shortcuts import get_object_or_404from django.views.decorators.http import require_http_methodsfrom example_project.helpers import parse_requestfrom 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_datetimeemployee.save()return HttpResponse(status=200)
在我們的示例視圖中,我們調(diào)用save而不調(diào)用clean或full_clean,這意味著傳遞到我們視圖中的clock_in_datetime可能發(fā)生在員工創(chuàng)建datetime__date之前,并且仍保存到數(shù)據(jù)庫中。這導(dǎo)致無效數(shù)據(jù)進入我們的數(shù)據(jù)庫。讓我們修復(fù)我們的錯誤。
employee.last_clock_in = clock_in_datetimeemployee.full_clean()employee.save()
現(xiàn)在,如果clock_in_datetime在員工的datetime_created之前,full_clean將引發(fā)ValidationError,以防止無效數(shù)據(jù)進入我們的數(shù)據(jù)庫。
保存時不包括update_fields
Django Model的save方法包括一個名為update_fields的關(guān)鍵字參數(shù)。在針對Django的典型生產(chǎn)環(huán)境中,人們使用gunicorn在同一臺計算機上運行多個Django服務(wù)器進程,并使用celery運行后臺進程。當調(diào)用不帶update_fields的保存時,整個模型將使用內(nèi)存中的值進行更新。讓我們看一下實際的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
一次調(diào)用不帶update_fields的保存將導(dǎo)致保存用戶模型上的每個字段。使用update_fields時,僅first_name更新。在頻繁寫入的生產(chǎn)環(huán)境中,在沒有update_fields的情況下調(diào)用save可能導(dǎo)致爭用情況。假設(shè)我們有兩個進程正在運行,一個運行我們的Django服務(wù)器的gunicorn工人和一個celery worker。按照設(shè)定的時間表,celery worker將查詢外部API,并可能更新用戶的is_active。
from celery import taskfrom django.contrib.auth import get_user_modelfrom example_project.external_api import get_user_statusUser = get_user_model()@taskdef 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 = Falseuser.save()
celery worker啟動任務(wù),將整個用戶對象加載到內(nèi)存中,并查詢外部API,但是外部API花費的時間比預(yù)期的長。當celery worker等待外部API時,同一用戶連接到我們的gunicorn worker,并向他們的電子郵件提交更新,將其更新從[email protected]更改為[email protected]。電子郵件更新提交到數(shù)據(jù)庫后,外部API響應(yīng),并且celery worker將用戶的is_active更新為False。
在這種情況下,celery worker會覆蓋電子郵件更新,因為該工作者會在提交電子郵件更新之前將整個用戶對象加載到內(nèi)存中。當celery worker將用戶加載到內(nèi)存中時,該用戶的電子郵件為[email protected]。該電子郵件將保留在內(nèi)存中,直到外部API響應(yīng)并覆蓋電子郵件更新為止。最后,代表數(shù)據(jù)庫內(nèi)部用戶的行包含舊電子郵件地址[email protected]和is_active = False。讓我們更改代碼以防止出現(xiàn)這種情況。
if user_status == "inactive":user.is_active = Falseuser.save(update_fields=["is_active"])
如果以前的情況是使用更新的代碼發(fā)生的,那么在celery worker更新is_active之后,用戶的電子郵件仍為[email protected],因為該更新僅寫入is_active字段。僅在極少數(shù)情況下(例如創(chuàng)建新對象),才應(yīng)調(diào)用不帶update_fields的保存。雖然可以通過不調(diào)用簡單的save方法在代碼庫中解決此問題,但第三方Django程序包可能包含此問題。例如,Django REST Framework不在PATCH請求上使用update_fields。Django REST Framework是我喜歡使用的出色軟件包,但無法解決此問題。將第三方軟件包添加到Django項目時,請記住這一點。
寫在最后
我已經(jīng)多次犯了所有這些錯誤。我希望這篇文章能揭示日常代碼中潛在的錯誤,并防止這些錯誤發(fā)生。我喜歡使用Django,而且我認為這是構(gòu)建Web應(yīng)用程序的非常好的框架。但是,在任何大型框架下,復(fù)雜性都會變得模糊不清,都可能會犯錯誤,該趟的坑一個也不會少。
(版權(quán)歸原作者所有,侵刪)
![]()

點擊下方“閱讀原文”查看更多
