跳轉到

處理找不到資料的錯誤

開始之前

任務目標

在這個章節中,我們會完成:

  • 了解 Article.objects.get() 的潛在問題
  • 認識 get_object_or_404 函式
  • 修改 article_detail 使用 get_object_or_404
  • 測試 404 錯誤頁面
  • 自訂 404 錯誤頁面
  • 了解錯誤處理的最佳實踐

問題:找不到資料時會發生什麼?

在前面的章節中,我們建立了 article_detail view 來顯示單一文章:

blog/views.py
def article_detail(request, article_id):
    article = Article.objects.get(id=article_id)
    return render(request, "blog/article_detail.html", {"article": article})

這個實作看起來沒問題,但有一個嚴重的問題:

如果文章不存在會發生什麼事?

試試看

使用 shell_plus 來模擬這個情況:

uv run manage.py 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 做了什麼?

get_object_or_404(Model, **kwargs)

這個函式會:

  1. 嘗試從資料庫取得資料
  2. 如果找到 → 回傳該物件
  3. 如果找不到 → 拋出 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()

blog/views.py
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. 在第 1 行匯入 get_object_or_404
  2. 在第 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 頁面

啟動開發伺服器

uv run manage.py runserver

測試正常情況

假設你的資料庫中有一篇 ID 為 1 的文章,訪問:http://127.0.0.1:8000/blog/articles/1/

如果文章存在,應該可以正常顯示文章內容。

測試 404 錯誤

訪問一個不存在的文章 ID:http://127.0.0.1:8000/blog/articles/999/

你會看到

Django 的預設 404 錯誤頁面,而不是 500 錯誤!

頁面會顯示類似:

Page not found (404)

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 函式的目的:

  1. 降低重複:這是非常常見的模式,不應該每次都寫 try-except
  2. 提高可讀性:函式名稱清楚表達意圖
  3. 減少錯誤:統一的做法減少遺漏錯誤處理的機會

這就是 Django 的「Don't Repeat Yourself (DRY)」原則!

最佳實踐

何時使用 get_object_or_404

應該使用

view 函式中查詢單一物件時:

def article_detail(request, article_id):
    article = get_object_or_404(Article, id=article_id)
    # ...

def comment_delete(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)
    # ...

不建議使用

templatemodel 方法中:

# ❌ 不要在 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 資料夾(如果還沒有的話):

mkdir -p 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(找到這兩行分別修改):

core/settings.py
DEBUG = False

ALLOWED_HOSTS = ["localhost", "127.0.0.1"]

ALLOWED_HOSTS 必須設定

DEBUG = False 時,ALLOWED_HOSTS 不能是空列表,必須明確指定允許的主機名稱。

否則會出現 「Invalid HTTP_HOST header」錯誤。

步驟 2:啟動伺服器並測試

uv run manage.py runserver

訪問不存在的頁面:http://127.0.0.1:8000/blog/articles/999/

你應該會看到自訂的 404 頁面,而不是預設的錯誤頁面!

步驟 3:記得改回 DEBUG = True

測試完後,記得將 DEBUG 改回 True

core/settings.py
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,讓你的程式碼更健壯、更專業。