在撰寫非同步 Rust 專案時,我一直關注相關工具和函式庫展。這些變化大多是正面的,特別是 Tokio 及其相關專案的進展令人印象深刻。

對於開發網頁服務,我推薦使用 axum 框架,它是 Tokio 專案的一部分。axum 框架相對精簡,但功能強大,這要歸功於其靈活的 API 和幾乎無巨集實作。它很容易與其他函式庫具整合,而與簡單的 API 使入門變得快速容易。

axum 根據 Tower(https://github.com/tower-rs/tower),一個提供構建網路服務抽象的函式庫及 hyper(https://hyper.rs/),它為 HTTP/1 和 HTTP/2 提供 HTTP 客戶端和伺服器實作。

axum 最好的地方在於它在模式或實踐方面幾乎不會強加太多限制。雖然要深入瞭解細節需要學習 Tower 函式庫對於簡單的任務,這並不必要。基本的網頁服務可以快速建立,無需花費大量時間學習框架。對於生產服務,axum 支援追蹤和指標,只需少量設定即可啟用。

其他值得一提的框架

還有兩個值得一提的框架是 Rocket(https://rocket.rs/)和 Actix(https://actix.rs/)。

Rocket 是一個旨在成為 Rust 版 Ruby on Rails 的網頁框架,而 Actix 是最早的 Rust 網頁框架之一。

這兩個框架都有同樣的問題:它們大量使用巨集隱藏實作細節。相比之下,axum 核心 API 不使用巨集這讓它更易於使用和理解。

值得稱讚的是,Rocket 和 Actix 都存在於 Rust 穩定 Future trait 和 async/await 語法之前——在那之前,使用巨集必要的。此外,這兩個框架在最新版本中都在減少對巨集依賴方面取得了進展。

設計服務架構

典型 Web 層架構

對於我們的網頁服務,我們將遵循典型的 Web 層架構,它至少包含三個元件:負載平衡器、網頁服務本身和狀態服務(即資料函式庫我們不會實作負載平衡器(假設它已經存在或已提供),而對於資料函式庫們將使用 SQLite,但在實際應用中,你可能想使用 PostgreSQL 等 SQL 資料函式庫 如圖所示,我們的 API 服務可以透過簡單地增加更多服務例項來水平擴充套件。每個 API 服務例項從負載平衡器接收請求,並獨立與資料函式庫以儲存和檢索狀態。

組態管理

我們的應用程式應該從環境中接收設定,所以我們將使用環境變數傳遞設定引數。我們也可以使用命令列解析或設定檔,但環境變數非常方便,特別是在佈署到叢集協調系統等環境時。在我們的例子中,我們只會使用幾個設定引數:一個指定資料函式庫一個設定日誌。

在大多數情況下,每個 API 服務例項的設定將是相同的,雖然在某些特殊情況下,你可能想指定每個服務例項獨有的引數,如位置訊息或要繫結的 IP 地址。在實務中,我們通常繫結到 0.0.0.0 地址,這會繫結到所有介面並將處理細節的工作有效地委託給作業系統網路堆積積(並可以根據需要進行設定)。

API 設計

待辦事項應用 API

對於我們的服務,我們將建立一個基本的待辦事項應用。我們只會實作待辦事項的建立、讀取、更新和刪除(CRUD)端點以及列表端點。我們還將增加活動和就緒健康檢查端點。我們會將 API 路由放在 /v1 路徑下,如下表所示:

路徑HTTP 方法操作請求體回應
/v1/todosGET列表所有待辦事項列表
/v1/todosPOST建立新待辦事項物件新建立的待辦事項物件
/v1/todos/:idGET讀取待辦事項物件
/v1/todos/:idPUT更新更新的待辦事項物件更新後的待辦事項物件
/v1/todos/:idDELETE刪除刪除的待辦事項物件

對於讀取、更新和刪除路徑,我們使用路徑引數作為每個待辦事項的 ID,在前面的路徑中用 :id 標記表示。我們還將增加活動和就緒健康檢查端點:

路徑HTTP 方法回應
/aliveGET成功時回傳 200 和 ok
/readyGET成功時回傳 200 和 ok

函式庫具選擇

依賴套件

我們將依賴現有的 crate 來完成大部分繁重的工作。實際上,我們不需要寫太多程式碼——我們主要是將現有元件組合在一起來構建服務。然而,我們必須密切關注如何組合不同的元件,但幸運的是,Rust 的類別系統透過編譯器錯誤告訴我們何時出錯,使這變得容易。

我們可以使用 cargo new api-server 初始化專案,之後我們可以使用 cargo add ... 增加所需的 crate。以下是我們需要的 crate 及其功能:

名稱功能描述
axum預設Web 框架
chronoserde日期/時間函式庫有 serde 功能
serdederive序列化/反序列化函式庫有 #[derive(…)] 功能
serde_json預設serde crate 的 JSON 序列化/反序列化
sqlxruntime-tokio-rustls, sqlite, chrono, macros用於 SQLite、MySQL 和 PostgreSQL 的非同步 SQL 工具包
tokiomacros, rt-multi-thread非同步執行時,與 axum 和 sqlx 一起使用

在開發過程中,我發現這些依賴組合能夠很好地協同工作,尤其是 axum 和 sqlx 的整合特別順暢。

在這個依賴列表中,每個套件都有其特定用途:

  • axum 是我們的 Web 框架,提供路由、處理請求和回應的基礎設施
  • chrono 處理日期和時間,並且 serde 整合以支援序列化
  • serdeserde_json 處理資料的序列化和反序列化,這在 REST API 中至關重要
  • sqlx 是一個非同步 SQL 工具包,我們用它來與 SQLite 資料函式庫
  • tokio 提供非同步執行時,是所有非同步操作的基礎

這些套件的組合使我們能夠建立一個高效能、非阻塞的 API 服務,同時保持程式碼的簡潔和可維護性。

資料模型設計

在實作 API 之前,我們需要定義待辦事項的資料模型。一個好的資料模型應該既能滿足應用需求,又能與資料函式庫和 API 表示層很好地比對。

待辦事項通常包含標題、描述、完成狀態和截止日期等欄位。我們還需要一個唯一識別碼來區分不同的待辦事項。在實際設計中,我會考慮以下欄位:

  1. id - 唯一識別碼
  2. title - 標題
  3. description - 描述(可選)
  4. completed - 完成狀態
  5. created_at - 建立時間
  6. updated_at - 最後更新時間
  7. due_date - 截止日期(可選)

這個模型既能滿足基本的待辦事項管理需求,又為未來可能的擴充套件留有空間。在下一節中,我們將實作這個資料模型,並建立相應的資料函式庫。

實作 API 服務

現在我們已經定義了資料模型和 API 設計,接下來可以開始實作我們的 API 服務。實作將包括以下幾個主要部分:

  1. 設定專案結構和設定
  2. 實作資料函式庫和模型
  3. 建立 API 路由和處理程式
  4. 實作錯誤處理
  5. 增加健康檢查端點

在下一部分中,我們將逐步實作這些功能,並展示如何使用 axum 框架和 sqlx 建立一個完整的 REST API 服務。我們將特別關注非同步處理和錯誤處理,這些是建立穩健 API 服務的關鍵方面。

值得注意的是,axum 的設計理念是提供高度可組合的元件,這使得我們能夠靈活地構建 API 服務,同時保持程式碼的清晰和可維護性。相比於其他更"魔法化"的框架,axum 的這種設計理念使得程式碼更容易理解和除錯。

在實作過程中,我們將展示如何利用 Rust 的強類別系統和模式比對能力來處理各種情況,包括錯誤處理和資料驗證,這些都是建立穩健 API 服務的關鍵方面。

Rust REST API開發:從依賴設定到應用框架

在現代後端開發中,Rust因其卓越的效能和安全性成為越來越受歡迎的選擇。今天我將帶大家深入瞭解如何使用Rust的Axum框架開發一個功能完整的HTTP REST API服務。這個實作不僅展示了Rust在Web開發領域的實力,也提供了一個可靠的範本,幫助你在實際專案中快速起步。

專案相依性管理

在開始API開發前,首先需要設定好專案依賴。使用Cargo這個Rust的包管理工具,我們可以輕鬆地增加和管理所需的依賴。對於需要啟用特定功能的依賴,可以使用--feature標記。

以下是我們API服務所需的核心依賴:

cargo add axum
cargo add chrono --features serde
cargo add serde --features derive
cargo add serde_json
cargo add sqlx --features runtime-tokio-rustls,sqlite,chrono,macros
cargo add tokio --features macros,rt-multi-thread
cargo add tower-http --features trace,cors
cargo add tracing
cargo add tracing-subscriber --features env-filter

這組命令增加了我們構建REST API所需的所有依賴。其中Axum是一個根據Tokio的現代Rust Web框架,特點是輕量與高效;SQLx提供非同步資料函式庫能力,特別啟用了SQLite支援;Tokio為我們提供非同步執行時;tower-http則提供了中介軟體功能如跟蹤和CORS支援。每個依賴都有特定的功能啟用,確保我們的API服務具備完整的功能集。

如果你想要更深入地使用SQLx,還可以安裝SQLx命令列工具:

cargo install sqlx-cli

這個工具可以讓你建立資料函式庫行遷移和刪除資料函式庫於開發過程中的資料函式庫非常有幫助。

安裝完所有依賴後,你的Cargo.toml檔案應該看起來像這樣:

[package]
name = "api-service"
version = "0.1.0"
edition = "2021"

[dependencies]
axum = "0.6.18"
chrono = { version = "0.4.26", features = ["serde"] }
serde = { version = "1.0.164", features = ["derive"] }
serde_json = "1.0.99"
sqlx = { version = "0.6.3", features = ["runtime-tokio-rustls", "sqlite", "chrono", "macros"] }
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.4.1", features = ["trace", "cors"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }

值得注意的是,在實際開發中,相依性管理常是一個動態過程。我們不一定要一開始就設定好所有依賴,而是可以根據開發過程中的需要逐步增加或修改。軟體開發的靈活性正是它的魅力所在,不要害怕根據你的需求調整範例。

應用程式框架搭建

現在,讓我們開始構建API服務的核心架構。我們的應用入口點在main.rs中,它包含了必要的設定步驟:

  1. 宣告主入口函式
  2. 初始化追蹤和日誌功能
  3. 建立並初始化資料函式庫
  4. 執行必要的資料函式庫
  5. 定義API路由
  6. 啟動服務

主函式實作

讓我們先看main()函式的實作:

#[tokio::main]
async fn main() {
    init_tracing();
    
    let dbpool = init_dbpool().await
        .expect("couldn't initialize DB pool");
        
    let router = create_router(dbpool).await;
    
    let bind_addr = std::env::var("BIND_ADDR")
        .unwrap_or_else(|_| "127.0.0.1:3000".to_string());
        
    axum::Server::bind(&bind_addr.parse().unwrap())
        .serve(router.into_make_service())
        .await
        .expect("unable to start server")
}

這個main()函式雖然簡潔,但包含了啟動API服務的所有必要步驟。首先使用#[tokio::main]巨集初始化Tokio執行時,這讓我們可以使用async/await語法。接著初始化追蹤功能,建立資料函式庫池,設定路由,最後啟動HTTP伺服器。

值得注意的是,我們使用環境變數BIND_ADDR來決定服務繫結的地址和連線埠如果沒有設定則預設使用127.0.0.1:3000。這種方式讓設定更加靈活,適合在不同環境中佈署。

使用tokio::main巨集以避免手動設定Tokio執行時的繁瑣工作。如果需要更精細的控制,比如設定工作執行緒量,可以透過環境變數TOKIO_WORKER_THREADS來實作,或者使用tokio::runtime::Builder手動設定執行時。

追蹤功能初始化

追蹤功能對於監控和除錯API服務至關重要。以下是init_tracing()函式的實作:

fn init_tracing() {
    use tracing_subscriber::{
        filter::LevelFilter, fmt, prelude::*, EnvFilter
    };
    
    let rust_log = std::env::var(EnvFilter::DEFAULT_ENV)
        .unwrap_or_else(|_| "sqlx=info,tower_http=debug,info".to_string());
        
    tracing_subscriber::registry()
        .with(fmt::layer())
        .with(
            EnvFilter::builder()
                .with_default_directive(LevelFilter::INFO.into())
                .parse_lossy(rust_log),
        )
        .init();
}

這個函式設定了應用程式的日誌和追蹤系統。它首先嘗試從環境變數RUST_LOG取得日誌設定,如果沒有設定則使用預設值"sqlx=info,tower_http=debug,info"。這表示SQLx的日誌級別為info,tower_http的日誌級別為debug,其他模組的預設級別為info。

在開發過程中,如果想要檢視所有追蹤訊息,可以設定RUST_LOG=trace,但要注意這會產生大量輸出,不適合在生產環境中使用。EnvFilter與許多Rust函式庫的env_logger相容,這保持了在Rust生態系統中的一致性和熟悉度。

資料函式庫池初始化

對於API服務,高效的資料函式倉管理至關重要。以下是init_dbpool()函式的實作:

async fn init_dbpool() -> Result<sqlx::Pool<sqlx::Sqlite>, sqlx::Error> {
    use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
    use std::str::FromStr;
    
    let db_connection_str =
        std::env::var("DATABASE_URL")
            .unwrap_or_else(|_| "sqlite:db.sqlite".to_string());
            
    let dbpool = SqlitePoolOptions::new()
        .connect_with(SqliteConnectOptions::from_str(&db_connection_str)?
            .create_if_missing(true))
        .await
        .expect("can't connect to database");
            
    sqlx::migrate!()
        .run(&dbpool)
        .await
        .expect("database migration failed");
        
    Ok(dbpool)
}

這個函式建立了一個SQLite資料函式庫池。連線池允許我們取得和重用資料函式庫,避免為每個請求建立新連線,從而提高效能。

函式首先從環境變數DATABASE_URL取得資料函式庫字元串,如果沒有設定則預設使用sqlite:db.sqlite,這會在當前工作目錄中開啟或建立db.sqlite檔案。接著使用SqlitePoolOptions建立連線池,並設定為如果資料函式庫不存在則自動建立。最後執行所有的資料函式庫,確保資料函式庫是最新的。

對於SQLite來說,連線池雖然提供了一些最佳化但由於SQLite本身執行在同一個程式中的後台執行緒,所以不像MySQL或PostgreSQL那樣網路連線的資料函式庫必要。然而,保持這種設計可以讓程式碼容易切換到其他資料函式庫。

資料函式庫與連線管理

在Web API開發中,資料函式庫和連線管理是關鍵考量點。我們使用SQLx作為Rust的非同步資料函式庫端,它支援多種資料函式庫,但在這個例子中我們選擇了SQLite作為我們的資料函式庫 SQLite是一個輕量級的資料函式庫閤中小型應用和原型開發。它不需要額外的伺服器程式,所有的資料都儲存在單個檔案中,使得佈署和管理變得非常簡單。

在我們的實作中,資料函式庫字元串從環境變數DATABASE_URL中取得,這讓我們可以在不同環境中靈活設定資料函式庫。例如,在開發環境中可能使用本地SQLite檔案,而在測試環境中可能使用記憶體料函式庫:memory:`)。

let db_connection_str = 
    std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite:db.sqlite".to_string());

SQLx的一個強大特性是它支援資料函式庫,這讓我們可以版本控制資料函式庫的變更。在我們的程式碼,sqlx::migrate!()巨集自動執行migrations目錄中的所有遷移檔案,確保資料函式庫是最新的。

sqlx::migrate!()
    .run(&dbpool)
    .await
    .expect("database migration failed");

遷移檔案通常是按順序命名的SQL檔案,例如:

  • ./migrations/20230101000000_create_users_table.sql
  • ./migrations/20230102000000_add_email_field.sql

每個遷移檔案包含了SQL陳述式,用於建立或修改資料函式庫。SQLx會跟蹤已經執行過的遷移,確保每個遷移只執行一次。

路由設定與API設計

雖然我們的程式碼段中沒有顯示路由設定的詳細實作,但從主函式中的create_router(dbpool)呼叫可以看出,路由設定是API服務的核心部分。在Axum中,路由設定通常涉及定義HTTP端點、處理函式和中介軟體。

一個典型的Axum路由設定可能看起來像這樣:

async fn create_router(dbpool: sqlx::Pool<sqlx::Sqlite>) -> Router {
    Router::new()
        .route("/health", get(health_check))
        .route("/api/users", get(list_users).post(create_user))
        .route("/api/users/:id", get(get_user).put(update_user).delete(delete_user))
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive())
        .with_state(dbpool)
}

這個函式建立了API的路由設定。它定義了不同的HTTP端點和對應的處理函式,還增加了兩個中介軟體層:一個用於HTTP請求的追蹤,一個用於處理CORS(跨源資源分享)。最後,它將資料函式庫池作為應用狀態傳遞給路由器。

在這個設定中,我們定義了以下端點:

  • /health - 健康檢查端點,只支援GET請求
  • /api/users - 使用者表端點,支援GET(取得列表)和POST(建立使用者
  • /api/users/:id - 特定使用者點,支援GET(取得使用者、PUT(更新使用者和DELETE(刪除使用者

Axum使用狀態管理來分享資料函式庫池等資源,這樣每個處理函式都可以存取這些資源,而不需要全域變數。

非同步處理與效能考量

Rust的非同步程式設計是它在Web後端開發中脫穎而出的關鍵原因之一。透過使用async/await語法和Tokio執行時,我們可以高效地處理大量並發請求,而不需要建立大量系統執行緒

在我們的API服務中,從主函式到資料函式庫

Rust API 開發:從資料函式庫到模型設計

在建構現代後端系統時,RESTful API 已成為標準實踐。而 Rust 語言因其卓越的效能和安全保證,逐漸成為開發高效能 API 服務的絕佳選擇。這篇文章將探討如何使用 Rust 建立一個完整的 RESTful API 服務,從資料函式庫設定到資料模型設計,一步實作 CRUD 功能。

資料函式庫池的實作與最佳化

資料函式庫池是高效能 API 服務的根本,它能夠有效管理資料函式庫資源,避免頻繁建立和銷毀連線所帶來的效能損耗。在我們的 Rust API 服務中,首先需要實作一個健壯的資料函式庫池。

讓我們看 init_dbpool() 函式的實作:

// src/main.rs 中的 init_dbpool() 函式
async fn init_dbpool() -> SqlitePool {
    // 嘗試讀取環境變數,若不存在則使用預設值
    let db_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite:db.sqlite".to_string());
    
    // 設定 SQLite 連線選項,包括自動建立資料函式庫   let conn_opts = SqliteConnectOptions::from_str(&db_url)?
        .create_if_missing(true);
        
    // 建立資料函式庫池
    let pool = SqlitePool::connect_with(conn_opts).await?;
    
    // 執行資料函式庫
    sqlx::migrate!("./migrations").run(&pool).await?;
    
    pool
}

這段程式碼完成了幾個關鍵任務:

  1. 環境設定讀取:從環境變數取得資料函式庫字串,若不存在則使用預設的 SQLite 本地檔案。這種方式讓服務在不同環境中保持彈性。

  2. **自動建立資料函式庫:透過 create_if_missing(true) 設定,確保第一次連線時自動建立資料函式庫,無需額外步驟。

  3. 連線池建立:使用 SQLx 的 API 建立資料函式庫池,而非單一連線,這對於處理併發請求至關重要。

  4. 資料函式庫:連線成功後立即執行資料函式庫,確保資料函式庫與程式碼同步。

在開發過程中,我注意到資料函式庫化是個關鍵步驟,它直接影響服務的穩定性。特別是在微服務架構中,自動化的資料函式庫化能大幅提高系統的可靠性和可維護性。

資料函式庫的重要性與風險

SQLx 提供了強大的遷移 API,可以自動化資料函式庫變更。這類別於其他 Web 框架如 Django 或 Rails 中的遷移系統。然而,遷移操作本質上是有狀態與具破壞性的,因此需要謹慎處理。

在處理資料函式庫時,需要特別注意以下幾點:

  1. 確保遷移指令碼正確與冪等的(可重複執行而不產生副作用)
  2. 理想情況下提供向上(建立)和向下(回復)兩種遷移指令碼3. 避免在生產環境中測試未經充分驗證的遷移指令碼 在實際專案中,我常建立專門的測試環境和流程,確保遷移指令碼佈署到生產環境前經過嚴格測試。這種做法雖然增加了前期工作量,但能有效避免資料函式庫破壞帶來的嚴重後果。

資料模型設計與實作

在我們的 API 服務中,選擇了一個簡單但實用的資料模型:待辦事項(Todo)。雖然簡單,但它展示了資料函式庫的核心原則。

SQL 資料表設計

首先,讓我們看待辦事項表的 SQL 結構:

-- migrations/20230701202642_todos.sql
CREATE TABLE IF NOT EXISTS todos (
    id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
    body TEXT NOT NULL,
    completed BOOLEAN NOT NULL DEFAULT FALSE,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

這個 SQL 結構體現了幾個關鍵設計原則:

  1. 唯一識別:使用自動遞增的整數作為主鍵,確保每條記錄都有唯一識別。在大型系統中,也可以考慮使用 UUID,尤其是在分散式環境中。

  2. 必要欄位約束:所有欄位都設為 NOT NULL,避免因空值產生的邏輯錯誤。

  3. 預設值設定:除了待辦事項內容外,其他欄位都有合理的預設值,簡化資料建立流程。

  4. 時間戳記追蹤:包含建立時間和更新時間兩個時間戳,有助於資料稽核和排序。

在實際專案中,我發現明確的資料結構設計能顯著減少後期維護成本。例如,在一個企業級專案中,僅因為缺少適當的時間戳欄位,我們就花費了大量時間重建資料歷史。

Rust 資料模型與 SQLx 整合

接下來,我們將 SQL 資料表對映到 Rust 結構體,實作程式碼與資料函式庫縫整合:

// src/todo.rs 中的 Todo 結構體
#[derive(Serialize, Clone, sqlx::FromRow)]
pub struct Todo {
    id: i64,
    body: String,
    completed: bool,
    created_at: NaiveDateTime,
    updated_at: NaiveDateTime,
}

這個結構體設計有幾個關鍵點:

  1. 衍生特性:使用 #[derive] 自動實作多個特性:

    • Serialize:來自 serde 函式庫許將結構體序列化為 JSON 等格式
    • Clone:允許建立結構體的副本
    • sqlx::FromRow:允許從資料函式庫結果直接轉換成結構體
  2. 類別對映:將 SQL 類別準確對映到 Rust 類別,如將 SQL TIMESTAMP 對映到 chrono::NaiveDateTime

  3. 欄位對應:結構體欄位名稱與資料表欄位完全一致,簡化對映關係。

在開發 API 系統時,我發現這種直接對映方式顯著提高了開發效率,同時也減少了類別轉換錯誤。特別是在處理日期時間時,使用 chrono 函式庫原生類別,提供了更豐富的日期操作功能。

實作 CRUD 操作

現在,讓我們實作資料的 CRUD(建立、讀取、更新、刪除)操作。首先是讀取操作:

// src/todo.rs 中的讀取操作實作
impl Todo {
    pub async fn list(dbpool: SqlitePool) -> Result<Vec<Todo>, Error> {
        query_as("select * from todos")
            .fetch_all(&dbpool)
            .await
            .map_err(Into::into)
    }
    
    pub async fn read(dbpool: SqlitePool, id: i64) -> Result<Todo, Error> {
        query_as("select * from todos where id = ?")
            .bind(id)
            .fetch_one(&dbpool)
            .await
            .map_err(Into::into)
    }
}

這段程式碼實作了兩個基本的讀取操作:

  1. 清單查詢list() 方法回傳所有待辦事項,使用 fetch_all() 方法取得多條記錄。

  2. 單項查詢read() 方法根據 ID 查詢單個待辦事項,使用 fetch_one() 方法確保只取得一條記錄。

  3. 引數繫結:使用 bind() 方法安全地將引數繫結到 SQL 查詢中,避免 SQL 注入攻擊。

  4. 錯誤處理:使用 map_err(Into::into) 將 SQLx 特定錯誤轉換為應用程式通用錯誤類別。

在實際應用中,我發現這種查詢方式非常靈活。例如,可以輕鬆擴充套件 list() 方法來支援分頁、排序和過濾,而不需要大幅修改基礎結構。

接下來,讓我們看寫入操作的實作:

// src/todo.rs 中的寫入操作實作
impl Todo {
    pub async fn create(
        dbpool: SqlitePool,
        new_todo: CreateTodo,
    ) -> Result<Todo, Error> {
        query_as("insert into todos (body) values (?) returning *")
            .bind(new_todo.body())
            .fetch_one(&dbpool)
            .await
            .map_err(Into::into)
    }
    
    pub async fn update(
        dbpool: SqlitePool,
        id: i64,
        updated_todo: UpdateTodo,
    ) -> Result<Todo, Error> {
        query_as(
            "update todos set body = ?, completed = ?, \
            updated_at = datetime('now') where id = ? returning *",
        )
        .bind(updated_todo.body())
        .bind(updated_todo.completed())
        .bind(id)
        .fetch_one(&dbpool)
        .await
        .map_err(Into::into)
    }
    
    pub async fn delete(dbpool: SqlitePool, id: i64) -> Result<(), Error> {
        query("delete from todos where id = ?")
            .bind(id)
            .execute(&dbpool)
            .await?;
        Ok(())
    }
}

這段程式碼實作了三個基本的寫入操作:

  1. 建立操作create() 方法接受 CreateTodo 類別的引數,該類別包含建立待辦事項所需的最小資訊(僅待辦事項內容)。使用 returning * 子句立即取得新建立的記錄。

  2. 更新操作update() 方法允許更新待辦事項的內容和完成狀態,同時自動更新 updated_at 時間戳。同樣使用 returning * 取得更新後的記錄。

  3. 刪除操作delete() 方法根據 ID 刪除待辦事項,執行成功時回傳空元組,表示操作成功但無需回傳資料。

  4. SQL 函式使用:在更新操作中使用 datetime('now') SQL 函式設定當前時間,讓資料函式庫時間計算,避免客戶端與資料函式庫不同步的問題。

這些方法都遵循了相似的模式:準備 SQL 查詢、繫結引數、執行查詢、處理結果。這種一致性使程式碼易於理解和維護。

在開發過程中,我發現使用 returning * 子句是個很好的實踐,它避免了需要進行額外查詢來取得操作結果的情況,提高了 API 效率。不過要注意,並非所有資料函式庫援這個功能(例如,MySQL 在某些版本中不支援)。