Можем ли мы изменить идиому execute around, чтобы использовать изменяемый self?

#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 здесь.