I'm using diesel-rs
with Postgres as backend, and I want to share a transaction between different trait methods.
The idea is, I have a trait called Repo
that is an abstraction over database query and result with one method
#[async_trait]
trait Repo<Q> for Postgres {
type Response;
async fn lookup(&mut self, query: Q) -> Result<Self::Response, RepoError>;
}
So the database struct can only be given some query Q
if it implements Repo<Q>
. This prevents caller from calling unplanned arbitrary queries. It also help me in testing since I can swap the backend to a mock by simply implementing required trait and swapping the object.
The problem is - I can use transaction inside lookup
's body
let query_result = self.connection.get()?.transaction(|conn| {
let record = /* db queries */;
Ok::<_, RepoError>(record)
})?;
Ok(query_result)
but I'm not sure how I would span this (|conn| { ... })
across two or more Repo
implementations.
I'm thinking of something like this
let TrQuery { tr, query } = get_user.lookup(TrQuery::new(tr, user_id)).await?;
...
let TrQuery { tr, query } = get_admin.lookup(TrQuery::new(tr, admin_id)).await?;
...
let TrQuery { tr, query } = get_book.lookup(TrQuery::new(tr, book_id)).await?;
where TrQuery
is
pub(crate) struct TrQuery<'qt, Query> {
pub(crate) tr: &'qt mut PooledConnection<ConnectionManager<PgConnection>>,
pub(crate) query: Query,
}
this way I can pass transaction
and receive it through the result and pass it to the next call.
That is - both Q
and Self::Response
from Repo
trait will be wrapped in TrQuery
.
So I thought maybe writing it like this
impl<'qt> Repo<TrQuery<'qt, Q>> for Postgres { ... }
would work, but half-way through the refactor I've had already dealt with a ton of lifetime wrangling which makes me think, perhaps I'm going about it the wrong way. The application is also multi-threaded so I have to be more careful.
What would be a better way of sharing transaction between these Repo
methods?