Sqlx transaction abstraction

⚓ rust    📅 2025-05-28    👤 surdeus    👁️ 5      

surdeus

Warning

This post was published 31 days ago. The information described in this article may have changed.

Hi, 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

Read full topic

🏷️ rust_feed