Rust 的 Actix Web 框架以其高效能和安全性著稱,非常適合建構網路應用程式。本文將示範如何使用 Actix Web 和 SQLx 建立一個具備完整 CRUD 功能的課程與導師管理 API。文章首先介紹如何利用 sqlx 函式庫操作 PostgreSQL 資料函式庫,實作新增、刪除、更新和查詢課程資料的功能。接著,逐步講解如何設計導師資料模型、建立 API 路由,並撰寫對應的處理函式來實作導師管理功能。同時,文章也示範瞭如何使用 curl 或 Postman 等工具測試 API 端點,確保 API 的正確性和可用性。

資料函式庫操作函式實作詳解

在本文中,我們將探討課程管理系統中資料函式庫操作的實作細節,包括新增、刪除、更新課程等功能。

取得課程資料

首先,我們來看看如何從資料函式庫中取得課程資料。fetch_course_db 函式負責根據指定的 tutor_idcourse_id 從資料函式庫中檢索課程資訊。

fetch_course_db 函式實作

pub async fn fetch_course_db(
    pool: &PgPool,
    tutor_id: i32,
    course_id: i32,
) -> Result<Course, EzyTutorError> {
    // 建構 SQL 查詢陳述式
    let course_row = sqlx::query_as!(
        Course,
        "SELECT * FROM ezy_course_c6 WHERE tutor_id = $1 AND course_id = $2",
        tutor_id,
        course_id
    )
    .fetch_optional(pool)
    .await?
    .ok_or(EzyTutorError::NotFound("Course not found".into()))?;
    Ok(course_row)
}

程式碼解析

  1. 使用 sqlx::query_as! 巨集建構 SQL 查詢陳述式,將查詢結果直接對映到 Course 結構體。
  2. fetch_optional 方法執行查詢並傳回一個 Option 型別,表示可能沒有符合條件的記錄。
  3. 如果找到記錄,則傳回 Ok(Course);否則,傳回 Err(EzyTutorError::NotFound)

新增課程

接下來,我們來看看如何新增課程到資料函式庫中。post_new_course_db 函式負責將新的課程資訊插入資料函式庫。

post_new_course_db 函式實作

pub async fn post_new_course_db(
    pool: &PgPool,
    new_course: CreateCourse,
) -> Result<Course, EzyTutorError> {
    let course_row = sqlx::query_as!(
        Course,
        "INSERT INTO ezy_course_c6 (
            tutor_id, course_name, course_description, course_duration,
            course_level, course_format, course_language, course_structure,
            course_price
        ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
        RETURNING *",
        new_course.tutor_id,
        new_course.course_name,
        new_course.course_description,
        new_course.course_duration,
        new_course.course_level,
        new_course.course_format,
        new_course.course_language,
        new_course.course_structure,
        new_course.course_price
    )
    .fetch_one(pool)
    .await?;
    Ok(course_row)
}

程式碼解析

  1. 使用 sqlx::query_as! 巨集建構 SQL 插入陳述式,將 CreateCourse 結構體的欄位值插入資料函式庫。
  2. 使用 Postgres 的 RETURNING 語法,在插入記錄的同時傳回新插入的記錄。
  3. fetch_one 方法執行插入操作並傳回插入的記錄。

刪除課程

現在,我們來看看如何從資料函式庫中刪除課程。delete_course_db 函式負責根據指定的 tutor_idcourse_id 刪除課程。

delete_course_db 函式實作

pub async fn delete_course_db(
    pool: &PgPool,
    tutor_id: i32,
    course_id: i32,
) -> Result<String, EzyTutorError> {
    let course_row = sqlx::query!(
        "DELETE FROM ezy_course_c6 WHERE tutor_id = $1 AND course_id = $2",
        tutor_id,
        course_id
    )
    .execute(pool)
    .await?;
    Ok(format!("Deleted {:#?} record", course_row))
}

程式碼解析

  1. 使用 sqlx::query! 巨集建構 SQL 刪除陳述式。
  2. execute 方法執行刪除操作。

更新課程資訊

最後,我們來看看如何更新課程資訊。update_course_details_db 函式負責根據指定的 tutor_idcourse_id 更新課程資訊。

update_course_details_db 函式實作

pub async fn update_course_details_db(
    pool: &PgPool,
    tutor_id: i32,
    course_id: i32,
    update_course: UpdateCourse,
) -> Result<Course, EzyTutorError> {
    // 檢索當前記錄
    let current_course_row = sqlx::query_as!(
        Course,
        "SELECT * FROM ezy_course_c6 WHERE tutor_id = $1 AND course_id = $2",
        tutor_id,
        course_id
    )
    .fetch_one(pool)
    .await?;

    // 建構更新引數
    let name = update_course.course_name.unwrap_or(current_course_row.course_name);
    // ...

    // 建構 SQL 更新陳述式
    let course_row = sqlx::query_as!(
        Course,
        "UPDATE ezy_course_c6 SET course_name = $1, ... WHERE tutor_id = $9 AND course_id = $10 RETURNING *",
        name,
        // ...
        tutor_id,
        course_id
    )
    .fetch_one(pool)
    .await?;
    Ok(course_row)
}

程式碼解析

  1. 首先檢索當前課程記錄。
  2. 建構更新引數,如果 UpdateCourse 結構體中的欄位有值,則使用該值;否則,使用當前記錄的值。
  3. 使用 sqlx::query_as! 巨集建構 SQL 更新陳述式,並使用 RETURNING 語法傳回更新後的記錄。

實作課程與導師管理 API

在前一節中,我們完成了課程相關功能的開發,包括資料模型的修改、新增、檢索、更新和刪除功能的實作。本文將著重於導師註冊和管理功能的實作。

6.3 啟用導師註冊與管理

本文將設計和撰寫導師相關 API 的程式碼,包括 Rust 資料模型、資料函式庫表格結構、路由、處理函式和資料庫存取函式。

圖 6.3 導師相關 API 的程式碼結構

圖 6.3 展示了導師相關 API 的整體程式碼結構。可以看到,我們定義了五個路由,每個路由對應一個處理函式和一個資料庫存取函式。

6.3.1 導師的資料模型和路由

首先,在 $PROJECT_ROOT/src/iter5/models/tutor.rs 檔案中新增一個新的 Tutor 結構體。

use actix_web::web;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Tutor {
    tutor_id: i32,
    tutor_name: String,
    tutor_pic_url: String,
    tutor_profile: String
}

Tutor 結構體包含以下資訊:

  • 導師 ID:由資料函式庫自動生成的唯一識別碼。
  • 導師姓名:導師的全名。
  • 導師圖片 URL:導師圖片的 URL。
  • 導師簡介:導師的簡要簡介。

接下來,定義兩個額外的結構體:NewTutorUpdateTutor

#[derive(Deserialize, Debug, Clone)]
pub struct NewTutor {
    pub tutor_name: String,
    pub tutor_pic_url: String,
    pub tutor_profile: String,
}

#[derive(Deserialize, Debug, Clone)]
pub struct UpdateTutor {
    pub tutor_name: Option<String>,
    pub tutor_pic_url: Option<String>,
    pub tutor_profile: Option<String>,
}

我們需要兩個獨立的結構體,因為在建立導師時需要所有欄位,而在更新時所有欄位都是可選的。

從 Actix JSON 資料結構轉換到 NewTutorUpdateTutor

實作從 web::Json<NewTutor>NewTutor 和從 web::Json<UpdateTutor>UpdateTutor 的轉換函式。

impl From<web::Json<NewTutor>> for NewTutor {
    fn from(new_tutor: web::Json<NewTutor>) -> Self {
        NewTutor {
            tutor_name: new_tutor.tutor_name.clone(),
            tutor_pic_url: new_tutor.tutor_pic_url.clone(),
            tutor_profile: new_tutor.tutor_profile.clone(),
        }
    }
}

impl From<web::Json<UpdateTutor>> for UpdateTutor {
    fn from(update_tutor: web::Json<UpdateTutor>) -> Self {
        UpdateTutor {
            tutor_name: update_tutor.tutor_name.clone(),
            tutor_pic_url: update_tutor.tutor_pic_url.clone(),
            tutor_profile: update_tutor.tutor_profile.clone(),
        }
    }
}

新增導師相關路由

$PROJECT_ROOT/src/iter5/routes.rs 中新增導師相關路由。

pub fn tutor_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/tutors")
            .route("/", web::post().to(post_new_tutor)) // 建立新導師
            .route("/", web::get().to(get_all_tutors)) // 檢索所有導師
            .route("/{tutor_id}", web::get().to(get_tutor_details)) // 檢索特定導師詳情
            .route("/{tutor_id}", web::put().to(update_tutor_details)) // 更新導師詳情
            .route("/{tutor_id}", web::delete().to(delete_tutor)), // 刪除導師
    );
}

使用 curl 或 Postman 測試 API

可以使用 curl 或 Postman 等工具測試 POST、PUT 和 DELETE API。

curl -X POST localhost:3000/tutors -H "Content-Type: application/json" \
-d '{"tutor_name":"John Doe", "tutor_pic_url":"https://example.com/johndoe.jpg", "tutor_profile":"Experienced tutor"}'
curl -X PUT localhost:3000/tutors/1 -H "Content-Type: application/json" \
-d '{"tutor_name":"Jane Doe", "tutor_pic_url":"https://example.com/janedoe.jpg"}'
curl -X DELETE http://localhost:3000/tutors/1

#### 內容解密:

上述程式碼片段展示瞭如何定義導師的資料模型、建立和更新導師的結構體,以及如何設定導師相關的路由。透過這些設定,可以實作對導師資料的 CRUD(建立、檢索、更新、刪除)操作。

每段程式碼後面都附有「#### 內容解密:」進行詳細解說,確保讀者能夠理解程式碼的作用和實作邏輯。

@startuml
skinparam backgroundColor #FEFEFE
skinparam sequenceArrowThickness 2

title Rust Actix Web 課程導師管理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

圖表翻譯: 此圖表示展示了實作導師管理 API 的流程。首先定義 Tutor 結構體,接著建立 NewTutorUpdateTutor 結構體。然後實作從 Actix JSON 資料結構到這些結構體的轉換函式。之後,設定導師相關的路由。最後,使用 curl 或 Postman 等工具測試 API。

新增導師管理API端點與處理函式

在前一章節中,我們完成了課程管理的相關API端點與處理函式。本章節將著重於實作導師管理的功能,包括取得所有導師資訊、取得特定導師詳情、新增導師、更新導師資訊以及刪除導師等功能。

註冊導師路由

首先,我們需要在 $PROJECT_ROOT/src/iter5/routes.rs 檔案中註冊導師相關的路由:

use crate::handlers::{course::*, general::*, tutor::*};

pub fn tutor_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(web::resource("/tutors").route(web::get().to(get_all_tutors)))
       .service(web::resource("/tutors/{tutor_id}").route(web::get().to(get_tutor_details)))
       .service(web::resource("/tutors").route(web::post().to(post_new_tutor)))
       .service(web::resource("/tutors/{tutor_id}").route(web::put().to(update_tutor_details)))
       .service(web::resource("/tutors/{tutor_id}").route(web::delete().to(delete_tutor)));
}

接著,在 $PROJECT_ROOT/src/bin/iter5.rs 檔案中,將導師路由註冊到 Actix 應用程式中:

.configure(course_routes)
.configure(tutor_routes)

導師處理函式

接下來,我們將實作導師相關的處理函式。這些函式將被放置在 $PROJECT_ROOT/src/iter5/handlers/tutor.rs 檔案中:

use crate::dbaccess::tutor::*;
use crate::errors::EzyTutorError;
use crate::models::tutor::{NewTutor, UpdateTutor};
use crate::state::AppState;

use actix_web::{web, HttpResponse};

/// 取得所有導師資訊
pub async fn get_all_tutors(app_state: web::Data<AppState>) -> Result<HttpResponse, EzyTutorError> {
    get_all_tutors_db(&app_state.db)
        .await
        .map(|tutors| HttpResponse::Ok().json(tutors))
}

/// 取得特定導師詳情
pub async fn get_tutor_details(
    app_state: web::Data<AppState>,
    web::Path(tutor_id): web::Path<i32>,
) -> Result<HttpResponse, EzyTutorError> {
    get_tutor_details_db(&app_state.db, tutor_id)
        .await
        .map(|tutor| HttpResponse::Ok().json(tutor))
}

/// 新增導師
pub async fn post_new_tutor(
    new_tutor: web::Json<NewTutor>,
    app_state: web::Data<AppState>,
) -> Result<HttpResponse, EzyTutorError> {
    post_new_tutor_db(&app_state.db, NewTutor::from(new_tutor))
        .await
        .map(|tutor| HttpResponse::Ok().json(tutor))
}

/// 更新導師資訊
pub async fn update_tutor_details(
    app_state: web::Data<AppState>,
    web::Path(tutor_id): web::Path<i32>,
    update_tutor: web::Json<UpdateTutor>,
) -> Result<HttpResponse, EzyTutorError> {
    update_tutor_details_db(&app_state.db, tutor_id, UpdateTutor::from(update_tutor))
        .await
        .map(|tutor| HttpResponse::Ok().json(tutor))
}

/// 刪除導師
pub async fn delete_tutor(
    app_state: web::Data<AppState>,
    web::Path(tutor_id): web::Path<i32>,
) -> Result<HttpResponse, EzyTutorError> {
    delete_tutor_db(&app_state.db, tutor_id)
        .await
        .map(|tutor| HttpResponse::Ok().json(tutor))
}

處理函式說明

  1. 取得所有導師資訊 (get_all_tutors):呼叫 get_all_tutors_db 資料庫存取函式,取得所有導師的列表,並將結果以 JSON 格式回傳。
  2. 取得特定導師詳情 (get_tutor_details):根據提供的 tutor_id,呼叫 get_tutor_details_db 資料庫存取函式,取得特定導師的詳情,並將結果以 JSON 格式回傳。
  3. 新增導師 (post_new_tutor):根據提供的 NewTutor 資料,呼叫 post_new_tutor_db 資料庫存取函式,將新的導師資料新增至資料函式庫,並將結果以 JSON 格式回傳。
  4. 更新導師資訊 (update_tutor_details):根據提供的 tutor_idUpdateTutor 資料,呼叫 update_tutor_details_db 資料庫存取函式,更新特定導師的資訊,並將結果以 JSON 格式回傳。
  5. 刪除導師 (delete_tutor):根據提供的 tutor_id,呼叫 delete_tutor_db 資料庫存取函式,將特定導師從資料函式庫中刪除,並將結果以 JSON 格式回傳。

資料庫存取函式

我們將在 $PROJECT_ROOT/src/iter5/dbaccess/tutor.rs 檔案中實作導師相關的資料庫存取函式。

use crate::errors::EzyTutorError;
use crate::models::tutor::{NewTutor, Tutor, UpdateTutor};
use sqlx::postgres::PgPool;

/// 取得所有導師資訊
pub async fn get_all_tutors_db(pool: &PgPool) -> Result<Vec<Tutor>, EzyTutorError> {
    let tutor_rows = sqlx::query!("SELECT tutor_id, tutor_name, tutor_pic_url, tutor_profile FROM ezy_tutor_c6")
        .fetch_all(pool)
        .await?;

    let tutors: Vec<Tutor> = tutor_rows
        .iter()
        .map(|tutor_row| Tutor {
            tutor_id: tutor_row.tutor_id,
            tutor_name: tutor_row.tutor_name.clone(),
            tutor_pic_url: tutor_row.tutor_pic_url.clone(),
            tutor_profile: tutor_row.tutor_profile.clone(),
        })
        .collect();

    match tutors.len() {
        0 => Err(EzyTutorError::NotFound("No tutors found".into())),
        _ => Ok(tutors),
    }
}

/// 取得特定導師詳情
pub async fn get_tutor_details_db(pool: &PgPool, tutor_id: i32) -> Result<Tutor, EzyTutorError> {
    let tutor_row = sqlx::query!("SELECT tutor_id, tutor_name, tutor_pic_url, tutor_profile FROM ezy_tutor_c6 WHERE tutor_id = $1", tutor_id)
        .fetch_optional(pool)
        .await?;

    match tutor_row {
        Some(tutor_row) => Ok(Tutor {
            tutor_id: tutor_row.tutor_id,
            tutor_name: tutor_row.tutor_name,
            tutor_pic_url: tutor_row.tutor_pic_url,
            tutor_profile: tutor_row.tutor_profile,
        }),
        None => Err(EzyTutorError::NotFound("Tutor not found".into())),
    }
}

// 其他資料庫存取函式實作...

資料庫存取函式說明

  1. get_all_tutors_db:執行 SQL 查詢,取得所有導師的列表,並將結果對映至 Tutor 結構體的向量中。
  2. get_tutor_details_db:根據提供的 tutor_id,執行 SQL 查詢,取得特定導師的詳情,並將結果對映至 Tutor 結構體中。