#rust #transactions
#Ржавчина #транзакции #rust-дизель
Вопрос:
Краткое резюме
Я не могу найти способ обернуть diesel
s transaction API в свой собственный API из-за использования дизелем идиомы «execute around» для обработки транзакций в сочетании с ее не- amp;mut self
аргументами. Это означает, что в настоящее время мне нужно создать некоторые функции, которые действительно должны изменять состояние, также не amp;mut self
и использовать внутреннюю изменчивость. Это кажется уродливым, и мне интересно, есть ли обходной путь.
Длинная версия
Библиотека rust diesel использует технику, подобную идиоме «execute around», для обработки транзакций. Например, использовать его для вставки двух имен в базу данных в транзакции будет выглядеть следующим образом.
pub fn perform_insert_two_names_transactionally<C:Connection>(
conn: amp;C,
name1: amp;str,
name2: amp;str) {
conn.transaction::<_, Error, _>(|| {
diesel::insert_into(users)
.values(name.eq(name1))
.execute(amp;conn)?;
diesel::insert_into(users)
.values(name.eq(name2))
.execute(amp;conn)?;
Ok(())
})?;
}
Подпись для transaction
функции
fn transaction<T, E, F>(amp;self, f: F) -> Result<T, E>
where
F: FnOnce() -> Result<T, E>,
E: From<Error>,
Мы можем создать упрощенную версию этого, чтобы нам не нужно было строить с помощью diesel и определять таблицы и т.д. На самом деле мы используем его только для проверки типа и заимствования.
pub struct Connection {}
pub enum ConnectionError {}
impl Connection {
pub fn add_user(amp;self, name: amp;str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(amp;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 resu<
}
fn begin_transaction(amp;self) {}
fn end_transaction(amp;self) {}
fn abort_transaction(amp;self) {}
}
pub fn perform_insert_two_names_transactionally(
conn: amp;Connection,
name1: amp;str,
name2: amp;str,
) -> Result<(), ConnectionError> {
conn.transaction(|| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})?;
Ok(())
}
Важно отметить, что add_user
, и transaction
функции не принимают amp;mut self
, просто amp;self
и это похоже на ложь. В моей версии я бы хотел, чтобы они принимали amp;mut self
, чтобы было более ясно, что они изменяют состояние приложения.
If we try changing the usage of amp;self
to amp;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 amp;mut Connection
, which can then be used to perform the mutable calls.
pub struct Connection {}
pub enum ConnectionError {}
impl Connection {
pub fn add_user(amp;mut self, name: amp;str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(amp;mut self, f: F) -> Result<T, E>
where
F: FnOnce(amp;mut Connection) -> Result<T, E>,
{
self.begin_transaction();
let result = f(self);
if result.is_ok() {
self.end_transaction();
} else {
self.abort_transaction();
}
return resu<
}
fn begin_transaction(amp;mut self) {}
fn end_transaction(amp;mut self) {}
fn abort_transaction(amp;mut self) {}
}
pub fn perform_insert_two_names_transactionally(
conn: amp;mut Connection,
name1: amp;str,
name2: amp;str,
) -> Result<(), ConnectionError> {
conn.transaction(|conn| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})?;
Ok(())
}
Основная проблема заключается в том, что при переносе транзакций diesel у нас нет доступа к begin_transaction
, end_transaction
и abort_transaction
— вместо этого нам нужно использовать diesel::Connection::transaction
функцию.
Упрощенная версия этого
pub struct DieselConnection {}
pub struct WrapperConnection {
pub conn:DieselConnection
}
pub enum ConnectionError {}
impl DieselConnection {
pub fn add_user(amp;self, name: amp;str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(amp;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 resu<
}
fn begin_transaction(amp;self) {}
fn end_transaction(amp;self) {}
fn abort_transaction(amp;self) {}
}
impl WrapperConnection {
pub fn add_user(amp;mut self, name: amp;str) -> Result<(), ConnectionError> {
self.conn.add_user(name)
}
pub fn transaction<T, E, F>(amp;mut self, f: F) -> Result<T, E>
where
F: FnOnce(amp;mut WrapperConnection) -> Result<T, E>,
{
self.conn.transaction( || { f(self) } )
}
}
pub fn perform_insert_two_names_transactionally(
conn: amp;mut WrapperConnection,
name1: amp;str,
name2: amp;str,
) -> Result<(), ConnectionError> {
conn.transaction(|conn| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})
}
также доступно на игровой площадке.
Однако это приводит к следующей ошибке:
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
опять же, это имеет смысл.
Я не думаю, что это можно обойти, поместив соединение в RefCell
или подобное, но я бы хотел, чтобы мне сказали, что я ошибаюсь.
На данный момент я только что отказался от обертывания API таким образом, который соответствует моим ожиданиям изменчивости — все мои функции практически все используют amp;self
и обходят необходимость изменчивости с помощью внутренней мутации, используя RefCell
свои собственные данные.
Есть ли какой-нибудь способ обернуть API, подобный diesel transaction API, и получить нужные мне типы изменчивости?
Ответ №1:
Мне удалось обойти это, отметив, что если бы мы могли клонировать обернутое соединение, мы могли бы записать оболочку транзакции как
impl WrapperConnection {
pub fn transaction<T, E, F>(amp;mut self, f: F) -> Result<T, E>
where
F: FnOnce(amp;mut WrapperConnection) -> Result<T, E>,
{
let conn_copy = self.conn.clone();
conn_copy.transaction( || { f(self) } )
}
}
(который компилируется, если вы добавляете #[derive(Clone)]
в DieselConnection
)
Но на самом деле базовое соединение не реализуется Clone
. Таким образом, вместо того, чтобы хранить a DieselConnection
в WrapperConnection
, мы можем сохранить a Rc<DieselConnection>
, который реализует Clone
.
Полный рабочий код:
use std::rc::Rc;
pub struct DieselConnection {}
pub struct WrapperConnection {
pub conn: Rc<DieselConnection>,
}
pub enum ConnectionError {}
impl DieselConnection {
pub fn add_user(amp;self, name: amp;str) -> Result<(), ConnectionError> {
Ok(())
}
pub fn transaction<T, E, F>(amp;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 resu<
}
fn begin_transaction(amp;self) {}
fn end_transaction(amp;self) {}
fn abort_transaction(amp;self) {}
}
impl WrapperConnection {
pub fn add_user(amp;mut self, name: amp;str) -> Result<(), ConnectionError> {
self.conn.add_user(name)
}
pub fn transaction<T, E, F>(amp;mut self, f: F) -> Result<T, E>
where
F: FnOnce(amp;mut WrapperConnection) -> Result<T, E>,
{
let temp_conn = self.conn.clone();
temp_conn.transaction(|| f(self))
}
}
pub fn perform_insert_two_names_transactionally(
conn: amp;mut WrapperConnection,
name1: amp;str,
name2: amp;str,
) -> Result<(), ConnectionError> {
conn.transaction(|conn| {
conn.add_user(name1)?;
conn.add_user(name2)?;
Ok(())
})
}
который можно найти на игровой площадке rust здесь.