處理找不到資料的錯誤¶
開始之前¶
任務目標
在這個章節中,我們會完成:
- 了解
Article.objects.get()的潛在問題 - 認識
get_object_or_404函式 - 修改
article_detail使用get_object_or_404 - 測試 404 錯誤頁面
- 自訂 404 錯誤頁面
- 了解錯誤處理的最佳實踐
問題:找不到資料時會發生什麼?¶
在前面的章節中,我們建立了 article_detail view 來顯示單一文章:
def article_detail(request, article_id):
article = Article.objects.get(id=article_id)
return render(request, "blog/article_detail.html", {"article": article})
這個實作看起來沒問題,但有一個嚴重的問題:
如果文章不存在會發生什麼事?
試試看¶
使用 shell_plus 來模擬這個情況:
# 試著取得一個不存在的文章
In [1]: Article.objects.get(id=999)
---------------------------------------------------------------------------
DoesNotExist: Article matching query does not exist.
會拋出錯誤!
當使用 Article.objects.get() 找不到資料時,會拋出 DoesNotExist 錯誤。
如果在 view 中沒有處理這個錯誤,使用者會看到 500 Internal Server Error!
為什麼這是個問題?¶
從使用者體驗的角度來看:
| 錯誤類型 | HTTP 狀態碼 | 意義 | 使用者感受 |
|---|---|---|---|
| 500 Internal Server Error | 500 | 伺服器內部錯誤 | 網站壞掉了! |
| 404 Not Found | 404 | 找不到資源 | 這個頁面不存在 |
正確的錯誤狀態碼很重要
- 500 錯誤讓使用者以為網站有 bug
- 404 錯誤讓使用者知道是「找不到資料」,不是網站問題
- 搜尋引擎也會根據狀態碼來處理(500 可能影響 SEO)
解決方案:使用 get_object_or_404¶
Django 提供了 get_object_or_404 函式來優雅地處理這個問題。
get_object_or_404 做了什麼?¶
這個函式會:
- 嘗試從資料庫取得資料
- 如果找到 → 回傳該物件
- 如果找不到 → 拋出
Http404例外(而不是DoesNotExist)
Http404 vs DoesNotExist
DoesNotExist:資料庫層級的錯誤,會導致 500 錯誤Http404:HTTP 層級的錯誤,會導致 404 錯誤
Django 會自動捕捉 Http404 並顯示 404 錯誤頁面給使用者。
修改 article_detail¶
修改 blog/views.py,匯入 get_object_or_404 並使用它來取代 Article.objects.get():
from django.shortcuts import get_object_or_404, render
from blog.models import Article
def article_list(request):
articles = Article.objects.all()
return render(request, "blog/article_list.html", {"articles": articles})
def article_detail(request, article_id):
article = get_object_or_404(Article, id=article_id)
return render(request, "blog/article_detail.html", {"article": article})
就這麼簡單!我們做了兩個修改:
- 在第 1 行匯入
get_object_or_404 - 在第 12 行把
Article.objects.get(id=article_id)改成get_object_or_404(Article, id=article_id)
參數說明
get_object_or_404(Article, id=article_id) 的參數:
- 第一個參數:
Article- 要查詢的 Model - 後續參數:
id=article_id- 查詢條件(使用 keyword arguments)
相當於 Article.objects.get(id=article_id),但會在找不到時回傳 404。
測試 404 頁面¶
啟動開發伺服器¶
測試正常情況¶
假設你的資料庫中有一篇 ID 為 1 的文章,訪問:http://127.0.0.1:8000/blog/articles/1/
如果文章存在,應該可以正常顯示文章內容。
測試 404 錯誤¶
訪問一個不存在的文章 ID:http://127.0.0.1:8000/blog/articles/999/
DEBUG 模式的 404 頁面
在開發環境中(DEBUG = True),Django 會顯示詳細的 404 除錯頁面,包括:
- 當前的 URL 路徑
- 已註冊的所有 URL patterns
- 為什麼找不到
在正式環境中(DEBUG = False),會顯示簡潔的 404 錯誤頁面。
其他查詢條件¶
get_object_or_404 支援任何 Model.objects.get() 支援的查詢條件。
使用其他欄位查詢¶
# 使用 slug 查詢
article = get_object_or_404(Article, slug="django-introduction")
# 使用多個條件
article = get_object_or_404(Article, author__name="Arthur", published=True)
使用 Q 物件¶
from django.db.models import Q
article = get_object_or_404(
Article,
Q(title__contains="Django") | Q(title__contains="Python")
)
和 objects.get() 用法完全相同
除了第一個參數是 Model 外,其他參數都和 objects.get() 一樣。
如果你知道怎麼用 objects.get(),就知道怎麼用 get_object_or_404()!
為什麼不用 try-except?¶
你可能會想:「為什麼不直接用 try-except 來捕捉 DoesNotExist?」
# 這樣也可以,但不推薦
from django.http import Http404
def article_detail(request, article_id):
try:
article = Article.objects.get(id=article_id)
except Article.DoesNotExist:
raise Http404("文章不存在")
return render(request, "blog/article_detail.html", {"article": article})
為什麼推薦使用 get_object_or_404?¶
| 比較項目 | try-except | get_object_or_404 |
|---|---|---|
| 程式碼長度 | 5 行 | 1 行 |
| 可讀性 | 需要理解例外處理 | 語意清晰 |
| 維護性 | 容易遺漏錯誤處理 | Django 標準做法 |
| 團隊協作 | 每個人寫法可能不同 | 統一的慣例 |
Django 的哲學
Django 提供 get_object_or_404 這類 shortcut 函式的目的:
- 降低重複:這是非常常見的模式,不應該每次都寫 try-except
- 提高可讀性:函式名稱清楚表達意圖
- 減少錯誤:統一的做法減少遺漏錯誤處理的機會
這就是 Django 的「Don't Repeat Yourself (DRY)」原則!
最佳實踐¶
何時使用 get_object_or_404¶
應該使用
在 view 函式中查詢單一物件時:
不建議使用
在 template 或 model 方法中:
# ❌ 不要在 model 方法中使用
class Author(models.Model):
def get_latest_article(self):
# 這裡用 get_object_or_404 不合適
# 因為這不是 view 層,不應該拋出 Http404
return Article.objects.filter(author=self).latest('created_at')
get_object_or_404 是專門給 view 層使用的工具。
錯誤處理原則¶
| 層級 | 找不到資料的處理方式 |
|---|---|
| View 層 | 使用 get_object_or_404,回傳 404 給使用者 |
| Model 層 | 使用 objects.get() 或 filter().first(),讓呼叫者決定如何處理 |
| Business Logic | 使用 try-except,根據業務邏輯處理 |
自訂 404 錯誤頁面¶
當 Django 拋出 Http404 錯誤時,預設會顯示內建的 404 頁面。但在實際的專案中,我們通常會想要自訂一個符合網站風格的 404 頁面。
為什麼要自訂 404 頁面?¶
預設的 404 頁面:
- 開發環境:顯示詳細的除錯資訊(很實用)
- 正式環境:顯示簡單的「Not Found」文字(不夠友善)
自訂的 404 頁面可以:
- 維持網站的品牌形象和設計風格
- 提供友善的錯誤訊息
- 引導使用者回到有效的頁面
- 提供搜尋功能或熱門連結
建立 404.html 模板¶
Django 會自動尋找專案根目錄的 templates/404.html 檔案。
首先,在專案根目錄建立 templates 資料夾(如果還沒有的話):
然後建立 404.html 檔案:
{% extends "base.html" %}
{% block title %}
找不到頁面 - Django 大冒險
{% endblock title %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-8 text-center">
<div class="my-5">
<h1 class="display-1 text-muted">
404
</h1>
<h2 class="mb-4">
找不到頁面
</h2>
<p class="lead text-muted mb-4">
抱歉,您要找的頁面不存在。
</p>
<div class="d-flex gap-3 justify-content-center">
<a href="{% url 'article_list' %}" class="btn btn-primary">
<i class="bi bi-house-door"></i> 回到首頁
</a>
<a href="{% url 'article_list' %}" class="btn btn-outline-secondary">
<i class="bi bi-file-earmark-text"></i> 瀏覽文章
</a>
</div>
</div>
</div>
</div>
{% endblock content %}
設計建議
自訂 404 頁面時可以包含:
- 清楚的說明:告訴使用者發生什麼事
- 友善的語氣:不要讓使用者覺得是他們的錯
- 導航連結:提供回到首頁或其他重要頁面的連結
- 搜尋功能:讓使用者可以搜尋想找的內容(進階功能)
- 幽默元素:可以用輕鬆的方式緩解使用者的挫折感
測試自訂 404 頁面¶
步驟 1:暫時關閉 DEBUG 模式¶
編輯 settings.py(找到這兩行分別修改):
ALLOWED_HOSTS 必須設定
當 DEBUG = False 時,ALLOWED_HOSTS 不能是空列表,必須明確指定允許的主機名稱。
否則會出現 「Invalid HTTP_HOST header」錯誤。
步驟 2:啟動伺服器並測試¶
訪問不存在的頁面:http://127.0.0.1:8000/blog/articles/999/
你應該會看到自訂的 404 頁面,而不是預設的錯誤頁面!
步驟 3:記得改回 DEBUG = True¶
測試完後,記得將 DEBUG 改回 True:
不要在正式環境開啟 DEBUG
DEBUG = True 會:
- 顯示詳細的錯誤訊息(包含原始碼和敏感資訊)
- 洩漏系統路徑和設定
- 嚴重的安全風險
正式環境務必設定 DEBUG = False!
其他錯誤頁面¶
除了 404.html,你也可以建立其他錯誤頁面:
| 檔案 | HTTP 狀態碼 | 說明 |
|---|---|---|
400.html |
400 Bad Request | 錯誤的請求 |
403.html |
403 Forbidden | 沒有權限 |
404.html |
404 Not Found | 找不到頁面 |
500.html |
500 Internal Server Error | 伺服器錯誤 |
所有這些檔案都放在 templates/ 資料夾中,Django 會自動使用它們。
500 錯誤頁面要特別注意
500.html 會在伺服器發生錯誤時顯示,此時可能無法正常載入資料庫或執行複雜的邏輯。
因此 500.html 應該:
- 不要使用資料庫查詢
- 不要使用複雜的 template tags
- 盡量簡單,避免在顯示錯誤頁面時又發生錯誤
常見問題¶
get_object_or_404 效能如何?¶
和 objects.get() 完全一樣,沒有額外的效能負擔。
它只是在 objects.get() 外面包了一層錯誤處理而已。
找不到時可以自訂錯誤訊息嗎?¶
get_object_or_404 會拋出標準的 Http404,無法自訂訊息。
如果需要自訂錯誤訊息,可以使用 try-except:
from django.http import Http404
def article_detail(request, article_id):
try:
article = Article.objects.get(id=article_id)
except Article.DoesNotExist:
raise Http404("抱歉,找不到這篇文章")
return render(request, "blog/article_detail.html", {"article": article})
但在大多數情況下,標準(自訂)的 404 頁面就足夠了。
任務結束¶
完成!
恭喜你完成了這個章節!現在你已經:
- 了解
Article.objects.get()的潛在問題 - 認識
get_object_or_404函式 - 修改
article_detail使用get_object_or_404 - 測試 404 錯誤頁面
- 自訂 404 錯誤頁面
- 了解錯誤處理的最佳實踐
養成好習慣
從現在開始,在 view 中查詢單一物件時:
- ✅ 使用
get_object_or_404 - ❌ 不要使用
objects.get()
這是 Django 開發的最佳實踐,也是專業 Django 開發者的標準做法!
記得在所有需要查詢單一物件的 view 中都使用 get_object_or_404,讓你的程式碼更健壯、更專業。