在建構高效能的 Web 應用時,有效地管理資料庫連線與處理檔案上傳是兩個關鍵的進階主題。本文將深入探討如何在 Actix-web 框架中,整合 r2d2 來建立資料庫連線池,並使用 awmp 套件優雅地處理 multipart/form-data 請求,最終實現一個能將上傳檔案及相關資料存入 PostgreSQL 資料庫的完整後端功能。

步驟一:高效的資料庫連線 - r2d2 連線池

在 Web 應用中,為每個請求都建立一個新的資料庫連線是極其低效且昂貴的操作。連線池 (Connection Pool) 透過維護一組可重複使用的資料庫連線,極大地提升了應用的效能和吞吐量。r2d2 是 Rust 生態中一個通用的連線池實作,我們將它與 Diesel 結合使用。

1. 專案依賴

確保您的 Cargo.toml 包含 r2d2 相關功能:

[dependencies]
diesel = { version = "2.0", features = ["postgres", "r2d2"] }
dotenv = "0.15"
# ... 其他依賴

2. 建立與註冊連線池

main.rs 中,我們初始化 r2d2 連線池,並將其註冊為 Actix-web 的應用程式共享資料 (App Data)。

use actix_web::{web, App, HttpServer};
use diesel::prelude::*;
use diesel::r2d2::{self, ConnectionManager};
use std::env;

// 定義連線池型別別名,方便使用
pub type DbPool = r2d2::Pool<ConnectionManager<PgConnection>>;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv::dotenv().ok();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 未設定");

    // 建立連線管理器
    let manager = ConnectionManager::<PgConnection>::new(database_url);
    
    // 建立連線池
    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("建立資料庫連線池失敗");

    println!("正在監聽 http://127.0.0.1:8080");
    HttpServer::new(move || {
        App::new()
            // 將連線池註冊為共享資料
            .app_data(web::Data::new(pool.clone()))
            // ... 註冊路由 ...
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

程式碼解說

  • ConnectionManager: 負責建立和管理 PostgreSQL 連線。
  • r2d2::Pool: 連線池本身,我們透過 builder 模式來建立它。
  • app_data(web::Data::new(pool.clone())): 將連線池的實例 (pool) 包裹在 web::Data 中,使其成為一個可在所有 handler 間共享的執行緒安全指標。

步驟二:處理檔案上傳 - multipart/form-data

現在,我們來建立一個 POST /cats 端點,用以接收包含貓咪名稱 (文字) 和貓咪圖片 (檔案) 的 multipart/form-data 請求。

1. 專案依賴

我們使用 awmp 這個 crate 來簡化 multipart 請求的處理。

[dependencies]
awmp = "0.8"
# ... 其他依賴

2. 資料庫模型與 Schema

我們需要一個新的 Diesel 模型來將資料插入資料庫。

// 在 src/models.rs
use crate::schema::cats;
use serde::Deserialize;

#[derive(Deserialize, Insertable)]
#[diesel(table_name = cats)]
pub struct NewCat {
    pub name: String,
    pub image_path: String,
}

#[derive(Insertable)] 讓此結構體可以直接被 Diesel 用於 insert_into 操作。

3. 編寫 POST Handler

use actix_web::{web, Error, HttpResponse, Responder};
use awmp::Parts;
use std::collections::HashMap;
// ... 引入 DbPool 和 NewCat ...

pub async fn add_cat(
    pool: web::Data<DbPool>,
    mut parts: Parts,
) -> Result<impl Responder, Error> {
    // 1. 處理檔案部分
    let file_path = parts
        .files
        .take("image") // "image" 是表單中 <input type="file"> 的 name
        .pop()
        .and_then(|f| f.persist_in("./static/images").ok()) // 將檔案儲存到指定路徑
        .unwrap_or_default(); // 若無檔案則為空字串

    // 2. 處理文字欄位部分
    let text_fields: HashMap<_, _> = parts.texts.as_pairs().into_iter().collect();
    let name = text_fields.get("name").unwrap_or(&"未命名").to_string();

    // 3. 建立要插入的資料
    let new_cat = NewCat {
        name,
        image_path: file_path.to_string_lossy().to_string(),
    };

    // 4. 從連線池取得連線,並在一個阻塞執行緒中執行資料庫操作
    let mut conn = pool.get().expect("無法從連線池取得連線");
    web::block(move || {
        diesel::insert_into(cats::table)
            .values(&new_cat)
            .execute(&mut conn)
    })
    .await? // 等待 web::block 完成
    .map_err(|_| HttpResponse::InternalServerError().finish())?; // 處理 Diesel 錯誤

    Ok(HttpResponse::Ok().body("成功新增貓咪"))
}

圖表解說:檔案上傳與資料庫寫入流程

此循序圖詳細展示了後端處理一個 multipart/form-data POST 請求的完整流程。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title 檔案上傳 POST 請求處理流程

participant "Client" as Client
participant "Actix-web" as Actix
participant "awmp" as Awmp
participant "Diesel" as Diesel
participant "Database" as DB

Client -> Actix : POST /cats (multipart/form-data)
Actix -> Awmp : 傳遞請求以解析 Parts
Awmp --> Actix : 回傳解析後的檔案與文字欄位
Actix -> Actix : 將檔案儲存至檔案系統
Actix -> Diesel : 準備 NewCat 資料結構
Actix -> Diesel : 呼叫 insert_into
Diesel -> DB : 執行 INSERT SQL 語句
DB --> Diesel : 回傳執行結果
Diesel --> Actix : 回傳 Result
Actix --> Client : 回應 200 OK 或錯誤

@enduml

4. 註冊路由

最後,在 main.rsHttpServer 設定中加入新的路由。

// ...
HttpServer::new(move || {
    App::new()
        .app_data(web::Data::new(pool.clone()))
        .service(
            web::scope("/api")
                .route("/cats", web::post().to(add_cat))
                // ... 其他 GET 路由 ...
        )
})
// ...

現在,您的 Actix-web 應用程式不僅能高效地管理資料庫連線,還具備了處理檔案上傳並將資料持久化的能力,為建構功能更豐富的 Web 服務打下了堅實的基礎。