Sqlx transaction abstraction
⚓ Rust 📅 2025-05-28 👤 surdeus 👁️ 12Hi, yall.
So I've been recently working with sqlx and I'm trying to figure out how to abstract queries over connections/transactions. What I wanted to have is a service that manages transactions and a dao that runs the sql statements. I got it to work, however I don't like my abstractions particularly much.
What I would have is a TransactionProvider like PgPool that service can use to execute commands.
pub struct SomeService<Provider> {
transactionManager: Provider,
}
impl<Provider: TransactionProvider> SomeService<Provider> {
pub async fn run_raw(&self) {
run_query(self.transactionManager.connection()).await;
}
pub async fn run_in_transaction(&self) {
let mut transaction = self.transactionManager.transaction().await;
run_query(transaction.get()).await;
run_query(transaction.get()).await;
transaction.commit().await.unwrap();
}
}
async fn run_query<'a, A>(a: A) -> ()
where
A: Acquire<'a, Database = Postgres> + Send + Sync,
{
let mut executor = a.acquire().await.unwrap();
sqlx::query("select 'hello world'")
.execute(&mut *executor)
.await
.unwrap();
}
This is a simplified version. I would actually have a Dao instead of functions directly, but the idea is the same. I either use connection directly through self.transaction.connection() or in a transaction through self.transactionManager.transaction().
To create this I would use TransactionProvider which is a simple wrapper over PgPool basic operations.
pub trait TransactionProvider {
type BasicConnection<'a>: Acquire<'a, Database = Postgres> + Send + Sync
where
Self: 'a;
type Transaction: GenericTransaction + Send + Sync;
fn connection<'a>(&'a self) -> Self::BasicConnection<'a>;
async fn transaction<'a>(&'a self) -> Self::Transaction;
}
impl TransactionProvider for PgPool {
type BasicConnection<'a> = &'a PgPool;
type Transaction = Transaction<'static, Postgres>;
fn connection<'a>(&'a self) -> Self::BasicConnection<'a> {
self
}
async fn transaction<'a>(&'a self) -> Self::Transaction {
self.begin().await.unwrap()
}
}
pub trait GenericTransaction {
type Connection<'a>: Acquire<'a, Database = Postgres> + Send + Sync
where
Self: 'a;
fn get<'a>(&'a mut self) -> Self::Connection<'a>;
async fn commit(self) -> Result<(), sqlx::Error>;
async fn rollback(self) -> Result<(), sqlx::Error>;
}
impl GenericTransaction for Transaction<'static, Postgres> {
type Connection<'a>
= &'a mut Self
where
Self: 'a;
fn get<'a>(&'a mut self) -> Self::Connection<'a> {
self
}
async fn commit(self) -> Result<(), sqlx::Error> {
self.commit().await
}
async fn rollback(self) -> Result<(), sqlx::Error> {
self.rollback().await
}
}
The issue is that it's a lot of work and it's not the best in terms of DX.
Has anyone figured out a better way? Or a better logic separation that works with sqxl?
2 posts - 2 participants
🏷️ rust_feed