線上課程平台的資料模型需要隨著業務需求的發展而演進。本文介紹的模型擴充,加入了課程描述、格式、價格、語言等資訊,使平台能提供更完善的課程資訊。為了支援這些新增欄位,資料函式庫指令碼也做了相應的調整。此外,API 設計也考量了建立和更新課程的差異性,設計了不同的資料結構,避免不必要的欄位修改,提升 API 的安全性和易用性。這些改進讓平台能更好地服務學員和講師,提供更優質的線上學習體驗。
強化課程建立與管理的資料模型
在進行課程相關 API 的開發時,我們需要對現有的資料模型進行擴充,以滿足更複雜的業務需求。本文將探討如何增強 Course 資料模型,以支援更豐富的課程資訊。
現有的 Course 資料模型
首先,讓我們檢視目前的 Course 資料模型,定義於 $PROJECT_ROOT/src/iter5/models/course.rs 中:
pub struct Course {
pub course_id: i32,
pub tutor_id: i32,
pub course_name: String,
pub posted_time: Option<NaiveDateTime>,
}
這個資料結構雖然簡單,但已經能夠滿足基本的課程資訊儲存需求。然而,為了提供更豐富的課程描述,我們需要對其進行擴充。
擴充 Course 資料模型
我們將為 Course 結構體新增以下屬性:
- 描述(Description):用於描述課程的文字資訊,以便潛在學生能夠決定是否適合他們。
- 格式(Format):課程可以有多種傳遞格式,例如自定進度的視訊課程、電子書格式或講師主導的面對面培訓。
- 課程結構(Structure of course):目前,我們允許講師上傳描述課程的檔案(例如 PDF 格式的小冊子)。
- 課程時長(Duration of course):課程的長度。通常以視訊課程的錄製時間、面對面培訓的時數或電子書的建議學習時數來描述。
- 價格(Price):以美元指定的課程價格。
- 語言(Language):由於我們的網頁應用程式導向國際觀眾,因此允許開設多種語言的課程。
- 級別(Level):表示課程針對的學生級別。可能的值包括初學者、中級和專家。
內容解密:
上述新增屬效能夠提供更豐富的課程資訊,有助於學生更好地瞭解課程內容和特點。
重構專案程式碼結構
在擴充資料模型之前,我們先對專案程式碼結構進行重構,以提高程式碼的可維護性和可讀性。
步驟一:在 mod.rs 檔案中新增模組宣告
在 $PROJECT_ROOT/src/iter5/dbaccess 和 $PROJECT_ROOT/src/iter5/models 資料夾下的 mod.rs 檔案中,新增以下程式碼:
pub mod course;
pub mod tutor;
內容解密:
這告訴 Rust 編譯器將 $PROJECT_ROOT/src/iter5/models 和 $PROJECT_ROOT/src/iter5/dbaccess 資料夾下的內容視為 Rust 模組。這樣,我們就可以在其他原始檔中參照和使用 Course 資料結構,例如:
use crate::models::course::Course;
步驟二:調整模組匯入路徑
根據給定的指示,調整 $PROJECT_ROOT/src/iter5/handlers/general.rs、$PROJECT_ROOT/src/bin/iter5/main.rs、$PROJECT_ROOT/src/iter5/dbaccess/course.rs、$PROJECT_ROOT/src/iter5/handlers/course.rs 和 $PROJECT_ROOT/src/iter5/routes.rs 中的模組匯入路徑。
內容解密:
這些步驟確保了模組之間的正確參照和依賴關係,從而避免編譯錯誤。
圖表翻譯:
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title 強化課程資料模型與API設計
package "課程資料模型" {
package "基本資訊" {
component [course_id] as id
component [tutor_id] as tutor
component [course_name] as name
}
package "擴充欄位" {
component [描述/格式] as desc
component [價格/語言] as price
component [時長/級別] as level
}
}
package "API 設計" {
component [CreateCourse DTO] as create
component [UpdateCourse DTO] as update
component [Actix Web 路由] as route
}
package "資料庫 Schema" {
component [PostgreSQL] as pg
component [Schema 更新] as schema
component [mod.rs 模組] as module
}
id --> desc : 模型擴充
tutor --> price
name --> level
create --> route : POST /courses
update --> route : PUT /courses/{id}
pg --> schema : ALTER TABLE
module --> route : 模組匯入
note right of create
特徵工程包含:
- 特徵選擇
- 特徵轉換
- 降維處理
end note
note right of eval
評估指標:
- 準確率/召回率
- F1 Score
- AUC-ROC
end note
@enduml此圖示展示了 Course 資料模型的擴充屬性。
圖表翻譯: 上述Plantuml圖表展示了 Course 結構體及其新增的屬性之間的關係。這些屬性共同構成了豐富的課程資訊,有助於學生更好地瞭解課程。
修改 Rust 資料模型
在下一小節中,我們將對 Rust 資料模型進行修改。
對資料模型進行變更
首先,我們從檔案匯入開始進行變更。以下是原始的匯入集合:
use actix_web::web;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
接下來,讓我們修改 Course 資料結構以包含我們希望捕捉的額外資料元素。以下是更新後的 Course 資料結構,在 $PROJECT_ROOT/src/iter5/models/course.rs 中定義:
#[derive(Serialize, Debug, Clone, sqlx::FromRow)]
pub struct Course {
pub course_id: i32,
pub tutor_id: i32,
pub course_name: String,
pub course_description: Option<String>,
pub course_format: Option<String>,
pub course_structure: Option<String>,
pub course_duration: Option<String>,
pub course_price: Option<i32>,
pub course_language: Option<String>,
pub course_level: Option<String>,
pub posted_time: Option<NaiveDateTime>,
}
注意,我們宣告了一個具有三個必填欄位的結構體:course_id、tutor_id 和 course_name。其餘欄位是可選的(由 Option<T> 型別表示)。這反映了資料函式庫中的課程記錄可能沒有這些可選欄位的值。
內容解密:
#[derive(Serialize, Debug, Clone, sqlx::FromRow)]自動派生了幾個特徵。Serialize使我們能夠將Course結構體的欄位傳回給 API 客戶端。Debug使我們能夠在開發週期中列印結構體值。Clone幫助我們在遵守 Rust 所有權模型的同時複製字串值。sqlx::FromRow使我們能夠在從資料函式庫讀取值時自動將資料函式庫記錄轉換為Course結構體。
建立新的資料結構以處理不同表示形式
如果我們檢視 Course 資料結構,會發現有幾個欄位,如 posted_time 和 course_id,我們計劃在資料函式庫層級自動生成。雖然我們需要這些欄位來完整表示 Course 記錄,但我們不需要這些值由 API 客戶端傳送。那麼,我們如何處理 Course 的不同表示形式?
讓我們建立一個單獨的資料結構,它只包含與前端相關的欄位,用於建立新課程。以下是新的 CreateCourse 結構體:
#[derive(Deserialize, Debug, Clone)]
pub struct CreateCourse {
pub tutor_id: i32,
pub course_name: String,
pub course_description: Option<String>,
pub course_format: Option<String>,
pub course_structure: Option<String>,
pub course_duration: Option<String>,
pub course_price: Option<i32>,
pub course_language: Option<String>,
pub course_level: Option<String>,
}
在這個結構體中,我們指定了建立新課程所需的必填欄位是 tutor_id 和 course_name,其餘欄位是可選的。然而,對於導師網頁服務,course_id 和 posted_time 也是建立新課程所需的必填欄位,這些將在內部自動生成。
內容解密:
- 我們為
CreateCourse自動派生了Deserialize特徵,而為Course結構體自動派生了Serialize特徵。 - 這是因為
CreateCourse結構體將用作將使用者輸入傳遞給網頁服務的資料結構,作為 HTTP 請求主體的一部分。
實作轉換函式
為了將 Actix Web 框架反序列化後的資料轉換為我們的 Rust 結構體,我們將實作 Rust 的 From 特徵來撰寫轉換函式:
impl From<web::Json<CreateCourse>> for CreateCourse {
fn from(new_course: web::Json<CreateCourse>) -> Self {
CreateCourse {
tutor_id: new_course.tutor_id,
course_name: new_course.course_name.clone(),
course_description: new_course.course_description.clone(),
course_format: new_course.course_format.clone(),
course_structure: new_course.course_structure.clone(),
course_level: new_course.course_level.clone(),
course_duration: new_course.course_duration.clone(),
course_language: new_course.course_language.clone(),
course_price: new_course.course_price,
}
}
}
這個轉換相對簡單,但如果轉換過程中可能出現錯誤,我們會使用 TryFrom 特徵而不是 From 特徵。
內容解密:
- 如果在轉換過程中呼叫傳回
Result型別的 Rust 標準函式庫函式(例如,將字串值轉換為整數),則可能會發生錯誤。 - 為了處理這種情況,可以使用
TryFrom特徵,並實作try_from函式,同時宣告在處理過程中出現問題時將傳回的錯誤型別。
use std::convert::TryFrom;
impl TryFrom<web::Json<CreateCourse>> for CreateCourse {
type Error = EzyTutorError;
fn try_from(new_course: web::Json<CreateCourse>) -> Result<Self, Self::Error> {
Ok(CreateCourse {
tutor_id: new_course.tutor_id,
course_name: new_course.course_name.clone(),
course_description: new_course.course_description.clone(),
course_format: new_course.course_format.clone(),
course_structure: new_course.course_structure.clone(),
course_level: new_course.course_level.clone(),
course_duration: new_course.course_duration.clone(),
course_language: new_course.course_language.clone(),
course_price: new_course.course_price,
})
}
}
更新課程資料模型與 API 邏輯
在前一章節中,我們已經成功建立了接收客戶端資料以建立新課程的方法。然而,當需要更新課程資料時,我們無法直接沿用 CreateCourse 結構體。原因在於更新課程時,我們不希望修改 tutor_id,以避免將某位導師建立的課程轉移給其他導師。此外,CreateCourse 中的 course_name 欄位是必填的,但更新課程時,我們不希望每次都強制使用者更新課程名稱。
建立 UpdateCourse 結構體
首先,我們需要建立一個新的結構體 UpdateCourse,使其更適合用於更新課程詳情。
#[derive(Deserialize, Debug, Clone)]
pub struct UpdateCourse {
pub course_name: Option<String>,
pub course_description: Option<String>,
pub course_format: Option<String>,
pub course_structure: Option<String>,
pub course_duration: Option<String>,
pub course_price: Option<i32>,
pub course_language: Option<String>,
pub course_level: Option<String>,
}
內容解密:
UpdateCourse結構體中的所有欄位均為可選 (Option<T>),這是為了提供良好的使用者經驗,因為在更新課程時,使用者可能只想修改部分資訊。- 透過使用
Option<T>,我們可以區分哪些欄位被明確指定為None(即不更新),哪些欄位被省略(未提供值)。
實作 From 特性
接下來,我們需要為 UpdateCourse 實作 From 特性,以便從 web::Json<UpdateCourse> 轉換為 UpdateCourse。
impl From<web::Json<UpdateCourse>> for UpdateCourse {
fn from(update_course: web::Json<UpdateCourse>) -> Self {
UpdateCourse {
course_name: update_course.course_name.clone(),
course_description: update_course.course_description.clone(),
course_format: update_course.course_format.clone(),
course_structure: update_course.course_structure.clone(),
course_level: update_course.course_level.clone(),
course_duration: update_course.course_duration.clone(),
course_language: update_course.course_language.clone(),
course_price: update_course.course_price,
}
}
}
內容解密:
- 此實作允許直接從 Actix-web 的
web::Json物件中提取UpdateCourse例項。 - 所有欄位都透過
clone()方法進行複製,以確保資料的所有權正確轉移。
修改資料函式庫指令碼
為了將新的欄位納入資料函式庫,我們需要在 $PROJECT_ROOT/src/iter5/dbscripts/course.sql 檔案中更新資料函式庫指令碼。
/* Drop tables if they already exist*/
drop table if exists ezy_course_c6;
/* Create tables. */
/* Note: Don't put a comma after last field */
create table ezy_course_c6
(
course_id serial primary key,
tutor_id INT not null,
course_name varchar(140) not null,
course_description varchar(2000),
course_format varchar(30),
course_structure varchar(200),
course_duration varchar(30),
course_price INT,
course_language varchar(30),
course_level varchar(30),
posted_time TIMESTAMP default now()
);
內容解密:
- 資料表名稱現在帶有
c6字尾,以便於不同章節的程式碼獨立測試。 - 新的資料欄位已加入至資料表結構中。
tutor_id和course_name欄位設有NOT NULL約束,以確保這些欄位在插入資料時必須提供值。course_id設為主鍵,而posted_time預設為目前時間,這些都是在資料函式庫層級強制執行的約束。
更新 API 路由
接下來,我們需要在 $PROJECT_ROOT/src/iter5/routes.rs 中修改路由組態,以包含更新課程的 API。
use crate::handlers::{course::*, general::*};
use actix_web::web;
pub fn general_routes(cfg: &mut web::ServiceConfig) {
cfg.route("/health", web::get().to(health_check_handler));
}
pub fn course_routes(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("/courses")
.route("", web::post().to(post_new_course))
.route("/{tutor_id}", web::get().to(get_courses_for_tutor))
.route("/{tutor_id}/{course_id}", web::get().to(get_course_details))
.route("/{tutor_id}/{course_id}", web::put().to(update_course_details))
.route("/{tutor_id}/{course_id}", web::delete().to(delete_course))
);
}
內容解密:
- 路由組態中使用了不同的 HTTP 方法來處理不同的請求:
POST用於建立新課程,GET用於檢索課程,PUT用於更新課程,DELETE用於刪除課程。 - 路由組態中明確指定了處理函式與對應的 HTTP 方法。