在建構任何需要與資料庫互動的 Web 應用時,如何高效、安全地管理資料庫連線是一個核心議題。為每個請求都建立一個新的連線不僅效能低落,也難以擴展。正確的做法是使用「連線池」(Connection Pool)。本文將深入探討如何在 Actix-web 應用中,整合 sqlx 來建立一個非同步的 PostgreSQL 連線池,並透過 Actix-web 強大的依賴注入機制,將其安全地共享給所有 API handlers。
步驟一:為何需要連線池?
在一個多執行緒的 Web 伺服器中,多個請求會並行發生。如果每個請求都嘗試建立自己的資料庫連線,將會產生巨大的開銷,並可能迅速耗盡資料庫的連線數限制。連線池預先建立並維護一組可用的資料庫連線。當 handler 需要存取資料庫時,它會從池中「借用」一個連線,使用完畢後再「歸還」,而非每次都經歷昂貴的連線建立與銷毀過程。這極大地提升了應用的吞吐量和回應速度。
步驟二:設定 sqlx 連線池
我們將在 main 函式中完成連線池的初始化,並將其注入到 Actix-web 的應用程式狀態中。
1. 專案依賴
確保您的 Cargo.toml 包含 sqlx (啟用 postgres 和 runtime-tokio-native-tls 功能) 和 dotenv。
[dependencies]
actix-web = "4"
sqlx = { version = "0.6", features = ["postgres", "runtime-tokio-native-tls", "macros"] }
dotenv = "0.15"
serde = { version = "1.0", features = ["derive"] }
# ...
2. 在 main 函式中建立與註冊連線池
// 在 src/main.rs
use actix_web::{web, App, HttpServer};
use dotenv::dotenv;
use sqlx::postgres::{PgPool, PgPoolOptions};
use std::env;
use std::sync::Mutex;
// 定義 AppState,其中包含我們的連線池
pub struct AppState {
pub db: PgPool,
// ... 其他狀態
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 1. 載入 .env 檔案
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL 未設定");
// 2. 建立 sqlx 連線池
let db_pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("無法建立資料庫連線池");
// 3. 將連線池包裹在 AppState 中,並註冊為共享資料
let shared_data = web::Data::new(AppState {
db: db_pool,
// ...
});
println!("正在監聽 http://127.0.0.1:3000");
HttpServer::new(move || {
App::new()
.app_data(shared_data.clone()) // 注入共享狀態
.configure(configure_routes)
})
.bind("127.0.0.1:3000")?
.run()
.await
}
程式碼解說:
PgPoolOptions::new().max_connections(5).connect(...): 我們使用PgPoolOptions來精細地設定連線池,例如最大連線數,然後非同步地建立連線池。web::Data::new(AppState { ... }):AppState結構體作為一個容器,持有所有需要在 handler 間共享的狀態。db: db_pool這行程式碼就是將連線池放入狀態中。.app_data(shared_data.clone()): 這是依賴注入的核心。Actix-web 會將這個web::Data<AppState>實例提供給所有註冊的 handler。
步驟三:在 Handler 中使用連線池
一旦連線池被註冊為應用程式資料,我們就可以在 handler 的參數中,透過 web::Data<AppState> 型別來存取它。
// 在 src/handlers.rs
use crate::{models::Course, state::AppState};
use actix_web::{web, HttpResponse, Responder};
use sqlx::PgPool;
// Handler 接收 web::Data<AppState> 作為參數
pub async fn get_courses_for_tutor(
app_state: web::Data<AppState>,
path: web::Path<i32>,
) -> impl Responder {
let tutor_id = path.into_inner();
// 直接從 app_state 中取得 db 連線池
let courses: Result<Vec<Course>, _> = sqlx::query_as!(
Course,
"SELECT * FROM ezy_course_c6 WHERE tutor_id = $1",
tutor_id
)
.fetch_all(&app_state.db) // 使用連線池執行查詢
.await;
match courses {
Ok(courses) => HttpResponse::Ok().json(courses),
Err(_) => HttpResponse::InternalServerError().finish(),
}
}
程式碼解說:
app_state: web::Data<AppState>: Actix-web 自動將我們在main函式中註冊的共享資料注入到這個參數中。&app_state.db: 我們可以直接從app_state中存取db欄位,也就是我們的PgPool連線池,並將其傳遞給sqlx的查詢函式。
圖表解說:依賴注入與連線池使用流程
此組件圖清晰地展示了 DbPool 如何被包含在 AppState 中,並透過 Actix-web 的依賴注入機制,最終被 Handler 所使用。
@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title 資料庫連線池依賴注入架構
package "Actix-web App" {
[HttpServer]
[App]
[Handler]
}
package "Shared State" {
[web::Data<AppState>] as WebData
[AppState]
[DbPool (sqlx::PgPool)] as DbPool
}
[HttpServer] o-- [App]
[App] -> WebData : .app_data()
[App] o-- [Handler] : .route()
WebData o-- [AppState]
[AppState] o-- DbPool
[Handler] ..> WebData : (依賴注入)
note on link: Handler 透過參數\n取得 web::Data<AppState>
[Handler] ..> DbPool : app_state.db
note on link: Handler 從 AppState\n中取得連線池以執行查詢
@enduml步驟四:在單元測試中模擬依賴注入
在進行單元測試時,我們也需要模擬這個依賴注入的過程,為被測試的 handler 提供一個真實的(或測試專用的)資料庫連線池。
#[cfg(test)]
mod tests {
use super::*;
// ... 其他 use ...
#[actix_rt::test]
async fn get_all_courses_success() {
dotenv().ok();
let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL 未設定");
let pool: PgPool = PgPool::connect(&database_url).await.unwrap();
// 在測試中同樣建立 AppState 和 web::Data
let app_state = web::Data::new(AppState { db: pool });
let tutor_id: web::Path<i32> = web::Path::from(1);
// 將模擬的依賴傳入 handler
let resp = get_courses_for_tutor(app_state, tutor_id).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}
透過這種方式,我們將資料庫連線的管理從每個 handler 中抽離出來,集中在 main 函式中進行初始化。Handler 只需聲明它需要一個 web::Data<AppState>,Actix-web 就會自動為其提供,這大大簡化了程式碼,並提升了應用的效能與可維護性。