在建構任何需要與資料庫互動的 Web 應用時,如何高效、安全地管理資料庫連線是一個核心議題。為每個請求都建立一個新的連線不僅效能低落,也難以擴展。正確的做法是使用「連線池」(Connection Pool)。本文將深入探討如何在 Actix-web 應用中,整合 sqlx 來建立一個非同步的 PostgreSQL 連線池,並透過 Actix-web 強大的依賴注入機制,將其安全地共享給所有 API handlers。

步驟一:為何需要連線池?

在一個多執行緒的 Web 伺服器中,多個請求會並行發生。如果每個請求都嘗試建立自己的資料庫連線,將會產生巨大的開銷,並可能迅速耗盡資料庫的連線數限制。連線池預先建立並維護一組可用的資料庫連線。當 handler 需要存取資料庫時,它會從池中「借用」一個連線,使用完畢後再「歸還」,而非每次都經歷昂貴的連線建立與銷毀過程。這極大地提升了應用的吞吐量和回應速度。

步驟二:設定 sqlx 連線池

我們將在 main 函式中完成連線池的初始化,並將其注入到 Actix-web 的應用程式狀態中。

1. 專案依賴

確保您的 Cargo.toml 包含 sqlx (啟用 postgresruntime-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 就會自動為其提供,這大大簡化了程式碼,並提升了應用的效能與可維護性。