在建構高效能的 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 或錯誤
@enduml4. 註冊路由
最後,在 main.rs 的 HttpServer 設定中加入新的路由。
// ...
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 服務打下了堅實的基礎。