如何创建兼容Sqlx连接池与事务的Actix-web服务器?
错误原因分析
你遇到的两个编译错误都是因为泛型约束不符合sqlx和Actix Web的类型要求:
Pool<Postgres>: Executor<'_>未满足:在sqlx 0.4.x中,PgPool本身并没有实现Executortrait,只有引用类型&PgPool才实现了这个trait。Pool<Postgres>: Copy未满足:PgPool是包含数据库连接资源的类型,内部通过Arc管理共享,但它本身不能被Copy(复制会导致资源管理混乱,所以sqlx没有为它实现Copytrait)。
解决方案(适配sqlx 0.4.x)
我们可以通过调整create_server的设计,让它同时支持生产环境的连接池和测试环境的事务,具体修改如下:
1. 修改create_server的泛型设计
将泛型参数改为接受一个工厂闭包,用于为每个worker提供数据库执行器实例。这样生产环境可以返回连接池的克隆,测试环境可以返回事务的所有权(因为测试服务器只需要一个worker):
pub fn create_server<F, E>( listener: TcpListener, db_factory: F, ) -> Result<Server, std::io::Error> where F: Fn() -> E + Clone + 'static, E: 'static, // 要求任意生命周期下,E的引用都能作为Postgres的Executor for<'a> &'a E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let server = HttpServer::new(move || { App::new() .data(db_factory()) // 每个worker调用工厂函数获取实例 .route("/", web::get().to(index)) }) .listen(listener)? .run(); Ok(server) }
2. 调整生产环境的server函数
传递一个返回PgPool克隆的闭包(PgPool的克隆是轻量的,因为内部是Arc):
pub async fn server(pool: PgPool) -> std::io::Result<()> { const PORT: usize = 8088; let listener = TcpListener::bind(format!("0.0.0.0:{}", PORT)).expect("Failed to create listener"); println!("Running on port {}", PORT); // 工厂闭包返回pool的克隆 create_server(listener, move || pool.clone()).unwrap().await }
3. 修改测试代码,使用单worker服务器
测试中,事务无法被克隆,所以我们设置服务器只运行一个worker,让工厂闭包只执行一次,直接返回事务的所有权:
#[cfg(test)] pub mod tests { use super::*; use std::net::TcpListener; #[actix_rt::test] async fn test_foo() { let pool = create_pool().await; let transaction = pool.begin().await.expect("Failed to create transaction"); let listener = TcpListener::bind("0.0.0.0:0").expect("Failed to create listener"); let server = create_server(listener, move || transaction) .expect("Failed to create server"); // 启动服务器 tokio::spawn(server); // 这里可以添加测试逻辑,比如用reqwest调用服务器的/api路径 // 测试完成后,事务会自动回滚,无需手动提交 } }
更简单的替代方案:升级sqlx到0.5+
如果可以升级sqlx版本到0.5.x,PgPool会直接实现Executor trait,同时我们可以简化泛型约束:
- 修改
Cargo.toml中的sqlx依赖:
sqlx = { version = "0.5.13", default-features = false, features = ["postgres", "runtime-async-std-native-tls"] }
- 修改
create_server的泛型约束:
pub fn create_server<E>( listener: TcpListener, db: E, ) -> Result<Server, std::io::Error> where E: Clone + 'static, for<'a> E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let server = HttpServer::new(move || { App::new() .data(db.clone()) .route("/", web::get().to(index)) }) .listen(listener)? .run(); Ok(server) }
- 测试代码中,设置服务器为单worker(因为事务无法克隆):
#[actix_rt::test] async fn test_foo() { let pool = create_pool().await; let transaction = pool.begin().await.expect("Failed to create transaction"); let listener = TcpListener::bind("0.0.0.0:0").expect("Failed to create listener"); // 手动创建服务器并设置1个worker,避免需要克隆事务 let server = HttpServer::new(move || { App::new() .data(transaction) .route("/", web::get().to(index)) }) .workers(1) // 关键:只启动一个worker,无需克隆事务 .listen(listener) .expect("Failed to create server") .run(); tokio::spawn(server); }
这样修改后,生产环境和测试环境的代码都能正常编译,同时满足你“测试时使用独立事务,生产时使用连接池”的需求。
内容的提问来源于stack exchange,提问作者Oskar Persson
相关产品推荐
相关产品推荐

