2

I have created an wrapper, DatabaseMiddlewareFactory, that wraps a service endpoint in a connection. That connection may have a transaction started on it or it may not. I've done this for Postgresql with no problem. The wrapper does this;

  • Get a connection from the global connecton pool
  • Start a transaction by executing the 'BEGIN' statement.
  • Add the connection as a Data<> to the request
  • Execute the request
  • If the result is success, execute the 'COMMIT' statement on the connection. If the result is not success, execute the 'ROLLBACK' statement on the connection.

The code looks like this:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    config::load_config();
    env_logger::init();

    // create connection pool and pass to each request handler so it can get a connection or block while waiting
    let connector = PoolConnector::from_config_lazy(CONFIG.clone())
        .await
        .ok()
        .expect("Failed to create connection pool");

    info!("Running server on {}", &CONFIG.server);
    HttpServer::new(move || {
        App::new()
            .service(web::scope("/health")
                .route("/alive", web::get().to(health::alive))
                .route("/ready", web::get().to(health::ready)))
            .wrap(DatabaseMiddlewareFactory::new(connector.clone())) 
            .configure(add_authenticated_routes)
    })
    .bind(&CONFIG.server)?
    .run()
    .await
}

This provides a lot of nice things:

  • Endpoint methods get a connection rather than a connection pool. They don't have to get a connection from the pool and then clean it up. They don't have to start a transaction and then decide whether to rollback or commit.
  • Read-only endpoints can get a connection without a transaction
  • Mutating endpoints get a connection with a transaction
  • All integration tests are wrapped in a connection and that transaction is rolled back after the test, so the database is not altered.

Put another way:

  • I don't want endpoints to have to get a connection from a pool; they only need a connection.
  • I don't want endpoints to have to start a transaction and then commit it; that breaks tests which need to rollback.
  • I don't want to have to repeat the boilerplate code that commits on success and rollsback on failure.
  • I want tests to always rollback.

Another way to handle this would be to use the TransactionManager and always hand the request a Transaction. However, that would be overkill for many of my endpoints, which are simple reads.

The code for a connection looks like this;

pub type NativeConnection = sqlx::pool::PoolConnection<Postgres>;

///
/// Wrapper for a native database connection.
/// This keeps track of nested transactions.
/// 
pub struct PoolConnection {
    connection: NativeConnection,
    transactions: i32
}

impl PoolConnection {
    ///
    /// Borrow the underlying native connection
    /// **returns:**
    /// - a mutable reference to the 
    ///   underlying native database connection
    /// 
    pub fn get(&mut self) -> &mut NativeConnection {
        return &mut self.connection;
    }

    ///
    /// Determine if a transaction if running on the connection
    /// **returns:**
    /// - true if a transaction if running
    /// - false if a transaction is not running
    /// 
    pub fn transaction(&self) -> bool {
        return self.transactions > 0;
    }

    ///
    /// Start a transaction on the connection.
    /// 
    /// - **returns**: 
    ///   - if successful, an Ok result of true 
    ///   - if unsuccessful then an Err result with an Error 
    ///     from the underlying database layer.
    /// 
    pub async fn begin(&mut self) -> Result<bool, Error> {
        match sqlx::query("BEGIN").execute(&mut self.connection).await {
            Ok(_) => { 
                self.transactions += 1; 
                Ok(true) 
            },
            Err(e) => {
                error!("Unable to begin transaction: {}", &e);
                Err(e)
            }
        }
    }


    ///
    /// Commit the connection's transaction started with begin().
    /// 
    /// - **returns**: 
    ///   - on success, an Ok result of true 
    ///     if a nested transaction is running on the connection, 
    ///     or an Ok result of false if no transaction is running 
    ///     on the connection.
    ///   - on failure, an Err result with an Error 
    ///     from the underlying database layer.
    /// 
    pub async fn commit(&mut self) -> Result<bool, Error> {
        if self.transactions > 0 {
            if self.transactions == 1 {
                match sqlx::query("COMMIT").execute(&mut self.connection).await {
                    Ok(_) => { 
                        self.transactions = 0; 
                        Ok(false) 
                    }
                    Err(e) => {
                        error!("Unable to commit transaction: {}", &e);
                        Err(e)
                    }
                }        
            } else {
                self.transactions -= 1;
                Ok(self.transaction())
            }
        } else {
            panic!("Attempt to commit on a connection with no transaction open.");
        } 
    }

    ///
    /// Rollback the connection's transaction started with begin().
    /// 
    /// - **returns**: 
    ///   - if successful, an Ok result of true 
    ///     if a nested transaction is running on the connection, 
    ///     or an Ok result of false if no transaction is running 
    ///     on the connection.
    ///   - if the rollback fails then an Err result with an Error 
    ///     from the underlying database layer.
    /// 
    pub async fn rollback(&mut self) -> Result<bool, Error> {
        if self.transactions > 0 {
            match sqlx::query("ROLLBACK").execute(&mut self.connection).await {
                Ok(_) => { 
                    self.transactions = 0; 
                    Ok(false) 
                },
                Err(e) => {
                    error!("Unable to rollback transaction: {}", &e);
                    Err(e)
                }
            }
        } else {
            panic!("Attempt to rollback on a connection with no transaction open.");
        } 
    }
}

This works well in Postgresql. I can execute the "BEGIN", "COMMIT" and "ROLLBACK" statements using sqlx::query().
The problem is that this works in Postgres, but the same code does NOT work with MySql. MySql does not want to execute 'BEGIN', 'COMMIT' or 'ROLLBACK' as part of a prepared statement.

So I change the code to us a fetch like this;

    pub async fn begin(&mut self) -> Result<bool, Error> {
            // match sqlx::query("BEGIN").execute(&mut self.connection).await {
            match (&mut self.connection).fetch_optional("BEGIN").await {
                Ok(_) => { 
                self.transactions += 1; 
                Ok(true) 
            },
            Err(e) => {
                error!("Unable to begin transaction: {}", &e);
                Err(e)
            }
        }
    }

If I change the code to use a fetch, then I get the error 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'RETURNING *' at line 1.

So I like this design that wraps each request in a connection, but the method I am using to start a transaction is not working with mysql. Is there something I am missing? Is there another way to do this same thing? Thank you for your help.

Ezward
  • 17,327
  • 6
  • 24
  • 32

1 Answers1

0

Ok, that issue I had was actually in another sql statement that happens after the connection is returned. So the 'BEGIN' does work; I now use a fetch rather than query. This is what I ended up with:

    ///
    /// Start a transaction on the connection.
    /// 
    /// - **returns**: 
    ///   - if successful, an Ok result of true 
    ///   - if unsuccessful then an Err result with an Error 
    ///     from the underlying database layer.
    /// 
    pub async fn begin(&mut self) -> Result<bool, Error> {
        //match self.connection.deref_mut().begin().await {
        match (&mut self.connection).fetch_optional("BEGIN").await {
        //match sqlx::query("COMMIT").execute(&mut self.connection).await {
            Ok(_) => { 
                self.transactions += 1; 
                Ok(true) 
            },
            Err(e) => {
                error!("Unable to begin transaction: {}", &e);
                Err(e)
            }
        }        
    }


    ///
    /// Commit the connection's transaction started with begin().
    /// 
    /// - **returns**: 
    ///   - on success, an Ok result of true 
    ///     if a nested transaction is running on the connection, 
    ///     or an Ok result of false if no transaction is running 
    ///     on the connection.
    ///   - on failure, an Err result with an Error 
    ///     from the underlying database layer.
    /// 
    pub async fn commit(&mut self) -> Result<bool, Error> {
        if self.transactions > 0 {
            if self.transactions == 1 {
                //match sqlx::query("COMMIT").execute(&mut self.connection).await {
                match (&mut self.connection).fetch_optional("COMMIT").await {
                    Ok(_) => { 
                        self.transactions = 0; 
                        Ok(false) 
                    }
                    Err(e) => {
                        error!("Unable to commit transaction: {}", &e);
                        Err(e)
                    }
                }        
            } else {
                self.transactions -= 1;
                Ok(self.transaction())
            }
        } else {
            panic!("Attempt to commit on a connection with no transaction open.");
        } 
    }

    ///
    /// Rollback the connection's transaction started with begin().
    /// 
    /// - **returns**: 
    ///   - if successful, an Ok result of true 
    ///     if a nested transaction is running on the connection, 
    ///     or an Ok result of false if no transaction is running 
    ///     on the connection.
    ///   - if the rollback fails then an Err result with an Error 
    ///     from the underlying database layer.
    /// 
    pub async fn rollback(&mut self) -> Result<bool, Error> {
        if self.transactions > 0 {
            // match sqlx::query("ROLLBACK").execute(&mut self.connection).await {
            match (&mut self.connection).fetch_optional("ROLLBACK").await {
                Ok(_) => { 
                    self.transactions = 0; 
                    Ok(false) 
                },
                Err(e) => {
                    error!("Unable to rollback transaction: {}", &e);
                    Err(e)
                }
            }
        } else {
            panic!("Attempt to rollback on a connection with no transaction open.");
        } 
    }
}
Ezward
  • 17,327
  • 6
  • 24
  • 32