在現代 Web 服務開發中,良好的錯誤處理機制至關重要。本文將以 Actix Web 框架和 SQLx 資料函式庫操作為例,示範如何使用 Rust 語言實作自定義錯誤處理,確保 API 的穩定性和易用性。首先,我們定義一個名為 EzyTutorError 的列舉型別作為自定義錯誤型別,並利用 From trait 將 sqlx::Error 等外部錯誤型別轉換為 EzyTutorError,實作統一的錯誤處理流程。接著,在資料庫存取函式中,使用 Result 型別傳回資料函式庫操作結果,並在錯誤發生時傳回特定的 EzyTutorError 變體。最後,在處理函式中,利用 map 方法處理資料函式庫操作的結果,並將錯誤傳播給 Actix Web 框架,使其自動將 EzyTutorError 轉換為相應的 HTTP 錯誤回應。同時,我們也需要更新測試指令碼,以驗證自定義錯誤處理機制在各種錯誤場景下的正確性。

自定義錯誤處理在API中的應用與實踐

在開發根據Actix Web框架的Web服務時,錯誤處理是一個至關重要的環節。本文將探討如何在API中實作自定義錯誤處理,以提高程式的穩健性和使用者經驗。

錯誤處理的重要性

當Web服務遇到錯誤時,如何優雅地處理並傳回有意義的錯誤訊息給客戶端,是衡量一個API設計好壞的重要標準。適當的錯誤處理機制不僅能夠幫助開發者快速定位問題,也能提升使用者對應用的信任度。

自定義錯誤型別的定義

首先,我們需要定義一個自定義的錯誤型別EzyTutorError,它將作為我們的錯誤處理基礎。在errors.rs檔案中,我們實作了From<SQLxError> trait,將sqlx函式庫的錯誤轉換為我們的自定義錯誤型別。

impl From<SQLxError> for EzyTutorError { }

這種轉換使得我們能夠統一處理來自不同來源的錯誤。

資料庫存取函式的錯誤處理

db_access.rs中,我們修改了資料庫存取函式,使其傳回一個Result型別,這樣就可以將錯誤傳播給呼叫者。例如,在取得課程詳情的函式中:

pub async fn get_course_details_db(pool: &PgPool, tutor_id: i32, course_id: i32) -> Result<Course, EzyTutorError> {
    // 準備SQL陳述式
    let course_row = sqlx::query!("SELECT tutor_id, course_id, course_name, posted_time FROM ezy_course_c5 where tutor_id = $1 and course_id = $2", tutor_id, course_id)
        .fetch_one(pool)
        .await;
    if let Ok(course_row) = course_row {
        // 執行查詢
        Ok(Course {
            course_id: course_row.course_id,
            tutor_id: course_row.tutor_id,
            course_name: course_row.course_name.clone(),
            posted_time: Some(chrono::NaiveDateTime::from(course_row.posted_time.unwrap())),
        })
    } else {
        Err(EzyTutorError::NotFound("Course id not found".into()))
    }
}

內容解密:

  • 此函式用於從資料函式庫中檢索特定課程的詳細資訊。
  • 使用sqlx::query!巨集準備SQL查詢陳述式,並透過fetch_one方法執行查詢。
  • 如果查詢成功,將結果對映到Course結構體並傳回。
  • 如果查詢失敗,傳回一個自定義的EzyTutorError::NotFound錯誤。

處理函式的錯誤處理

handlers.rs中,我們更新了處理函式,使其能夠處理來自資料庫存取函式的錯誤。例如,在取得課程詳情的處理函式中:

pub async fn get_course_details(
    app_state: web::Data<AppState>,
    path: web::Path<(i32, i32)>,
) -> Result<HttpResponse, EzyTutorError> {
    let (tutor_id, course_id) = path.into_inner();
    get_course_details_db(&app_state.db, tutor_id, course_id)
        .await
        .map(|course| HttpResponse::Ok().json(course))
}

內容解密:

  • 此處理函式負責呼叫資料庫存取函式以取得課程詳情。
  • 使用.map方法將成功的結果轉換為HTTP回應。
  • 如果資料庫存取函式傳回錯誤,該錯誤將被傳播給Actix Web框架,並轉換為相應的HTTP錯誤回應。

測試指令碼的更新

為了確保錯誤處理機制正常工作,我們需要更新測試指令碼以涵蓋錯誤場景。例如,在測試取得課程詳情時:

#[actix_rt::test]
async fn get_course_detail_test() {
    // ...
    let resp = get_course_details(app_state, path).await;
    // 對resp進行斷言
}

內容解密:

  • 測試函式模擬呼叫get_course_details處理函式。
  • 需要對傳回的回應進行斷言,以驗證錯誤處理是否正確。

自定義錯誤處理在 Actix Web 中的應用

在開發 Web 服務時,錯誤處理是一個非常重要的環節。一個好的錯誤處理機制可以幫助我們更好地處理各種錯誤情況,並向客戶端傳回有意義的錯誤資訊。在本章中,我們將學習如何在 Actix Web 中實作自定義錯誤處理。

為什麼需要自定義錯誤處理?

Actix Web 提供了一套基本的錯誤處理機制,但是在實際開發中,我們往往需要根據具體的業務需求自定義錯誤處理邏輯。自定義錯誤處理可以幫助我們更好地控制錯誤資訊的格式和內容,並提供更友好的使用者經驗。

自定義錯誤型別的定義

首先,我們需要定義一個自定義的錯誤型別。在我們的例子中,我們定義了一個名為 EzyTutorError 的錯誤型別,它包含了多種可能的錯誤情況,例如資料函式庫錯誤、未找到錯誤等。

// 定義自定義錯誤型別
pub enum EzyTutorError {
    // 資料函式庫錯誤
    DbError(sqlx::Error),
    // 未找到錯誤
    NotFound(String),
    // 其他錯誤
    OtherError(String),
}

實作 ResponseError trait

為了讓 Actix Web 能夠自動將我們的自定義錯誤型別轉換為 HTTP 回應,我們需要實作 ResponseError trait。

// 實作 ResponseError trait
impl actix_web::error::ResponseError for EzyTutorError {
    fn error_response(&self) -> HttpResponse {
        match self {
            EzyTutorError::DbError(err) => {
                HttpResponse::InternalServerError().json(err.to_string())
            }
            EzyTutorError::NotFound(msg) => {
                HttpResponse::NotFound().json(msg)
            }
            EzyTutorError::OtherError(msg) => {
                HttpResponse::BadRequest().json(msg)
            }
        }
    }
}

內容解密:

此段程式碼實作了 ResponseError trait,讓 Actix Web 能夠根據 EzyTutorError 的不同變體傳回相應的 HTTP 回應。其中:

  • DbError 變體傳回 500 Internal Server Error。
  • NotFound 變體傳回 404 Not Found。
  • OtherError 變體傳回 400 Bad Request。 這確保了錯誤能夠以標準化的 HTTP 回應傳回給客戶端。

將其他錯誤型別轉換為自定義錯誤型別

我們還需要實作 From trait,以便將其他錯誤型別(例如 sqlx::Error)轉換為我們的自定義錯誤型別。

// 將 sqlx::Error 轉換為 EzyTutorError
impl From<sqlx::Error> for EzyTutorError {
    fn from(err: sqlx::Error) -> Self {
        EzyTutorError::DbError(err)
    }
}

內容解密:

此實作將 sqlx::Error 轉換為 EzyTutorError::DbError,使資料函式庫操作過程中發生的錯誤能夠被統一處理。這樣,當資料函式庫操作失敗時,可以傳回一個結構化的錯誤回應給客戶端。

修改資料庫存取函式和處理函式

接下來,我們需要修改資料庫存取函式和處理函式,以傳回我們的自定義錯誤型別。

// 修改資料庫存取函式
pub async fn post_new_course_db(
    pool: &PgPool,
    new_course: Course,
) -> Result<Course, EzyTutorError> {
    let course_row = sqlx::query!(
        "insert into ezy_course_c5 (course_id, tutor_id, course_name) values ($1, $2, $3) returning tutor_id, course_id, course_name, posted_time",
        new_course.course_id,
        new_course.tutor_id,
        new_course.course_name,
    )
    .fetch_one(pool)
    .await?;
    
    Ok(Course {
        course_id: course_row.course_id,
        tutor_id: course_row.tutor_id,
        course_name: course_row.course_name.clone(),
        posted_time: Some(chrono::NaiveDateTime::from(course_row.posted_time.unwrap())),
    })
}

// 修改處理函式
pub async fn post_new_course(
    new_course: web::Json<Course>,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, EzyTutorError> {
    post_new_course_db(&app_state.db, new_course.into())
        .await
        .map(|course| HttpResponse::Ok().json(course))
}

圖表翻譯:

圖表翻譯:
此圖示展示了客戶端請求在系統中的處理流程。客戶端發起請求後,處理函式呼叫資料庫存取函式進行資料函式庫操作。如果操作成功,則傳回成功回應;如果失敗,則傳回自定義錯誤。Actix Web 再將此自定義錯誤轉換為適當的 HTTP 回應傳回給客戶端。

重構專案結構與無懼的重構

本章涵蓋以下主題:

  • 重整專案結構
  • 增強課程建立與管理的資料模型
  • 啟用導師註冊與管理功能

在前一章中,我們討論了Rust中的錯誤處理,以及如何為我們的Web服務設計自定義的錯誤處理機制。經過前幾章的學習,您現在應該對如何使用Actix Web框架構建Web服務有了一定的瞭解,包括如何與關聯式資料函式庫進行CRUD操作,以及如何處理在處理傳入資料和請求時可能發生的錯誤。在本章中,我們將加快進度,處理現實世界中不可避免的問題:變化。

每個活躍的Web服務或應用程式在其生命週期中都會根據使用者反饋或業務需求進行重大演變。許多新的需求可能意味著對Web服務或應用程式的重大更改。在本章中,您將學習Rust如何幫助您應對涉及重大設計變更和重寫大量現有程式碼的情況。您將利用Rust編譯器的強大功能和語言特性,順利完成這一挑戰。

在本章中,您將對Web服務進行多項更改。您將重新設計課程的資料模型,新增課程路由,修改處理函式和資料庫存取函式,並更新測試案例。同時,您還將設計和構建應用程式中的新模組,以管理導師資訊並定義導師與課程之間的關係。此外,您還將增強Web服務的錯誤處理功能,以涵蓋邊緣情況。最後,您將徹底重構專案程式碼和目錄結構,以便在Rust模組之間整齊地分隔程式碼。

6.1 重整專案結構

在前一章中,我們專注於建立和維護基本的課程資料。在本章中,我們將增強課程模組並新增功能以建立和維護導師資訊。隨著程式碼函式庫的大小增長,是時候重新思考專案結構了。因此,我們將首先將專案重組為一個結構,這將有助於隨著應用程式變得更大、更複雜而進行程式碼開發和維護。

圖6.1顯示了兩個檢視。左邊是我們將要開始的專案結構(第5章的結構)。右邊是我們最終將得到的結構。

主要變化是,在提出的專案結構中,資料庫存取、處理函式和模型不再是單個檔案,而是資料夾。課程和導師的資料庫存取程式碼將被組織在dbaccess資料夾下。對於模型和處理函式也是如此。這種方法將減少個別檔案的長度,同時使導航到我們正在尋找的內容更加快捷,儘管它增加了專案結構的複雜性。

專案結構描述

在開始之前,讓我們設定PROJECT_ROOT環境變數,指向專案根目錄(ezytutors/tutor_db)的完整路徑:

export PROJECT_ROOT=<full-path-to ezytutors/tutor-db folder>

透過以下命令驗證是否正確設定:

echo $PROJECT_ROOT

從此以後,術語「專案根目錄」將指代儲存在$PROJECT_ROOT環境變數中的資料夾路徑。本章中對其他檔案的參照將相對於專案根目錄進行。

圖6.1展示了第5章和第6章的專案結構。

程式碼結構描述

  • $PROJECT_ROOT/src/bin/iter5.rs:包含main()函式。
  • $PROJECT_ROOT/src/iter5/routes.rs:包含路由。將繼續是一個包含所有路由的單個檔案。
  • $PROJECT_ROOT/src/iter5/state.rs:應用程式狀態,包含注入到每個應用程式執行緒中的依賴項。
  • $PROJECT_ROOT/src/iter5/errors.rs:自定義錯誤資料結構和相關的錯誤處理函式。
  • $PROJECT_ROOT/.env:包含資料庫存取憑據的環境變數。該檔案不應被提交到程式碼倉函式庫。
  • $PROJECT_ROOT/src/iter5/dbscripts:Postgres的資料函式庫表建立指令碼。
  • $PROJECT_ROOT/src/iter5/handlers
    • $PROJECT_ROOT/src/iter5/handlers/course.rs:與課程相關的處理函式。
    • $PROJECT_ROOT/src/iter5/handlers/tutor.rs:與導師相關的處理函式。
    • $PROJECT_ROOT/src/iter5/handlers/general.rs:健康檢查處理函式。
    • $PROJECT_ROOT/src/iter5/handlers/mod.rs:將handlers目錄轉換為Rust模組,以便Rust編譯器知道如何找到依賴的檔案。
  • $PROJECT_ROOT/src/iter5/models
    • $PROJECT_ROOT/src/iter5/models/course.rs:與課程相關的資料結構和實用方法。
    • $PROJECT_ROOT/src/iter5/models/tutor.rs:與導師相關的資料結構和實用方法。
    • $PROJECT_ROOT/src/iter5/models/mod.rs:將models目錄轉換為Rust模組,以便Rust編譯器知道如何找到依賴的檔案。
  • $PROJECT_ROOT/src/iter5/dbaccess
    • $PROJECT_ROOT/src/iter5/dbaccess/course.rs:與課程相關的資料庫存取方法。
    • $PROJECT_ROOT/src/iter5/dbaccess/tutor.rs:與導師相關的資料庫存取方法。
    • $PROJECT_ROOT/src/iter5/dbaccess/mod.rs:將dbaccess目錄轉換為Rust模組,以便Rust編譯器知道如何找到依賴的檔案。

重構步驟

  1. $PROJECT_ROOT/src/bin/iter4.rs 重新命名為 $PROJECT_ROOT/src/bin/iter5.rs

  2. $PROJECT_ROOT/src/iter4 資料夾重新命名為 $PROJECT_ROOT/src/iter5

  3. $PROJECT_ROOT/src/iter5 下建立三個子資料夾:dbaccessmodelshandlers

  4. $PROJECT_ROOT/src/iter5/models.rs 移動並重新命名為 $PROJECT_ROOT/src/iter5/models/course.rs

  5. $PROJECT_ROOT/src/iter5/models 資料夾下建立兩個額外的檔案:tutor.rsmod.rs。目前將這兩個檔案留空。

@startuml
skinparam backgroundColor #FEFEFE
skinparam sequenceArrowThickness 2

title API自定義錯誤處理實踐

actor "客戶端" as client
participant "API Gateway" as gateway
participant "認證服務" as auth
participant "業務服務" as service
database "資料庫" as db
queue "訊息佇列" as mq

client -> gateway : HTTP 請求
gateway -> auth : 驗證 Token
auth --> gateway : 認證結果

alt 認證成功
    gateway -> service : 轉發請求
    service -> db : 查詢/更新資料
    db --> service : 回傳結果
    service -> mq : 發送事件
    service --> gateway : 回應資料
    gateway --> client : HTTP 200 OK
else 認證失敗
    gateway --> client : HTTP 401 Unauthorized
end

@enduml

圖表翻譯: 此圖示展示了重構專案結構的主要步驟。首先,我們建立新的資料夾來組織程式碼。接著,將相關檔案移動到這些新資料夾中並進行重新命名。最後,更新這些檔案的內容以適應新的結構。

繼續重構

  1. $PROJECT_ROOT/src/iter5/dbaccess.rs 移動並重新命名為 $PROJECT_ROOT/src/iter5/dbaccess/course.rs

  2. $PROJECT_ROOT/src/iter5/dbaccess 資料夾下建立兩個額外的檔案:tutor.rsmod.rs。目前將這兩個檔案留空。

  3. $PROJECT_ROOT/src/iter5/handlers.rs 移動並重新命名為 $PROJECT_ROOT/src/iter5/handlers/course.rs

  4. $PROJECT_ROOT/src/iter5/handlers 資料夾下建立三個額外的檔案: tutor.rs, general.rs, 和 mod.rs. 目前將這三個檔案留空。

  5. 建立一個名為 $PROJECT_ROOT/src/iter5/dbscripts 的資料夾。將專案資料夾中現有的 database.sql 檔案移動到此目錄,並將其重新命名為 course.sql. 稍後我們將修改此檔案。

驗證專案結構

在此階段,請確保您的專案結構看起來與圖6.1所示的相似。接下來,我們將修改現有的程式碼以適應這個新的結構。