Use a transaction manager trait from your specific database driver (like sqlx, diesel, or tokio-postgres) to wrap database operations in a single atomic block, ensuring that either all changes commit or none do if an error occurs. You typically acquire a connection, start a transaction, execute your queries, and then explicitly commit or let the transaction abort if a panic or error propagates.
Here is a practical example using sqlx with PostgreSQL, which is the most common pattern for async Rust applications:
use sqlx::{PgPool, Error};
async fn transfer_funds(pool: &PgPool, from_id: i32, to_id: i32, amount: i64) -> Result<(), Error> {
// Start a transaction
let mut tx = pool.begin().await?;
// Deduct from sender
sqlx::query("UPDATE accounts SET balance = balance - $1 WHERE id = $2")
.bind(amount)
.bind(from_id)
.execute(&mut *tx)
.await?;
// Add to receiver
sqlx::query("UPDATE accounts SET balance = balance + $1 WHERE id = $2")
.bind(amount)
.bind(to_id)
.execute(&mut *tx)
.await?;
// Commit the transaction
tx.commit().await?;
Ok(())
}
If any query fails or the function returns an Err before commit() is called, the transaction automatically rolls back. You can also explicitly roll back using tx.rollback().await? if you need to handle a specific error case before exiting.
For synchronous applications or when using diesel, the pattern is similar but relies on the TransactionManager trait. Here is how you might handle it with diesel:
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, PooledConnection};
type DbConnection = PooledConnection<ConnectionManager<SqliteConnection>>;
fn update_records(conn: &mut DbConnection) -> Result<(), diesel::result::Error> {
conn.transaction(|conn| {
// All operations inside this closure are part of the transaction
diesel::sql_query("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
.execute(conn)?;
diesel::sql_query("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
.execute(conn)?;
Ok(()) // Returning Ok commits; returning Err rolls back
})
}
Key things to remember: always check the result of every query inside the transaction block, and avoid long-running operations that might hold locks for too long. If you are using async code, ensure you don't await on a transaction handle across a .await point if your driver doesn't support it, though modern drivers like sqlx handle this safely. If a panic occurs, the transaction will not commit, but you should still ensure your application logic handles the resulting rollback gracefully.