Short summary
I'm unable to find a way to wrap diesels transaction API in my own API, due to diesels use of the "execute around" idiom for transaction handling coupled with its non-&mut self arguments.
This means I currently have to make some functions which really should mutate state also non-&mut self and use interior mutability. This feels ugly and I'm wondering if there is a workaround.
Long version
The rust diesel library uses a technique like the "execute around" idiom for handling transactions. For example, to use it to insert two names into a database within a transaction would look like this.
pub fn perform_insert_two_names_transactionally<C:Connection>(
conn: &C,
name1: &str,
name2: &str) {
conn.transaction::<_, Error, _>(|| {
diesel::insert_into(users)
.values(name.eq(name1))
.execute(&conn)?;
diesel::insert_into(users)
.values(name.eq(name2))
.execute(&conn)?;
Ok(())
})?;
}
The signature for the transaction function is
fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
where
F: FnOnce() -> Result<T, E>,
E: From<Error>,
We can create a simplified version of this so that we don't need to build with diesel and define tables etc. We're really only using it for type and borrow checking.
pub struct Connection {}
pub enum ConnectionError {}
impl Connection {
pub fn add_user(&self, name: &str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
where
F: FnOnce() -> Result<T, E>,
{
self.begin_transaction();
let result = f();
if result.is_ok() {
self.end_transaction();
} else {
self.abort_transaction();
}
return result;
}
fn begin_transaction(&self) {}
fn end_transaction(&self) {}
fn abort_transaction(&self) {}
}
pub fn perform_insert_two_names_transactionally(
conn: &Connection,
name1: &str,
name2: &str,
) -> Result<(), ConnectionError> {
conn.transaction(|| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})?;
Ok(())
}
The key thing to note is that the add_user, and transaction functions do not take &mut self, just &self and this feels like a lie. In my version, I'd like them to take &mut self so that it is more clear that they are changing the application state.
If we try changing the usage of &self to &mut self (see this code) we get the following error:
error[E0501]: cannot borrow `*conn` as mutable because previous closure requires unique access
--> src/lib.rs:32:5
|
32 | conn.transaction(|| {
| ^ ----------- -- closure construction occurs here
| | |
| _____| first borrow later used by call
| |
33 | | conn.add_user(name1)?;
| | ---- first borrow occurs due to use of `conn` in closure
34 | | conn.add_user(name2)?;
35 | | Ok(())
36 | | })?;
| |______^ second borrow occurs here
We can get around this constraint by changing the signature of the function passed to transaction to accept a &mut Connection, which can then be used to perform the mutable calls.
pub struct Connection {}
pub enum ConnectionError {}
impl Connection {
pub fn add_user(&mut self, name: &str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
where
F: FnOnce(&mut Connection) -> Result<T, E>,
{
self.begin_transaction();
let result = f(self);
if result.is_ok() {
self.end_transaction();
} else {
self.abort_transaction();
}
return result;
}
fn begin_transaction(&mut self) {}
fn end_transaction(&mut self) {}
fn abort_transaction(&mut self) {}
}
pub fn perform_insert_two_names_transactionally(
conn: &mut Connection,
name1: &str,
name2: &str,
) -> Result<(), ConnectionError> {
conn.transaction(|conn| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})?;
Ok(())
}
The core problem is that when wrapping the diesel transactions we don't have access to begin_transaction, end_transaction and abort_transaction -- we need to use the diesel::Connection::transaction function instead.
The simplified version of this is
pub struct DieselConnection {}
pub struct WrapperConnection {
pub conn:DieselConnection
}
pub enum ConnectionError {}
impl DieselConnection {
pub fn add_user(&self, name: &str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(&self, f: F) -> Result<T, E>
where
F: FnOnce() -> Result<T, E>,
{
self.begin_transaction();
let result = f();
if result.is_ok() {
self.end_transaction();
} else {
self.abort_transaction();
}
return result;
}
fn begin_transaction(&self) {}
fn end_transaction(&self) {}
fn abort_transaction(&self) {}
}
impl WrapperConnection {
pub fn add_user(&mut self, name: &str) -> Result<(), ConnectionError> {
self.conn.add_user(name)
}
pub fn transaction<T, E, F>(&mut self, f: F) -> Result<T, E>
where
F: FnOnce(&mut WrapperConnection) -> Result<T, E>,
{
self.conn.transaction( || { f(self) } )
}
}
pub fn perform_insert_two_names_transactionally(
conn: &mut WrapperConnection,
name1: &str,
name2: &str,
) -> Result<(), ConnectionError> {
conn.transaction(|conn| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})
}
also available on the playground.
However, this gives the following error:
error[E0500]: closure requires unique access to `self` but it is already borrowed
--> src/lib.rs:38:32
|
38 | self.conn.transaction( || { f(self) } )
| --------- ----------- ^^ ---- second borrow occurs due to use of `self` in closure
| | | |
| | | closure construction occurs here
| | first borrow later used by call
| borrow occurs here
error: aborting due to previous error; 1 warning emitted
again, this makes sense.
I don't think this can be worked around by placing the connection in a RefCell or similar - but I'd love to be told I'm wrong.
At the moment I've just given up on wrapping the API in a way that matches my mutability expectations - my functions are all pretty much all using &self and work around the need for mutability using interior mutation using RefCell on their own data.
Is there some way wrap an API like the diesel transaction API and get the mutability types that I want?