Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
177: Transactions in SQLx
How do I actually start and finish a transaction?
In SQLx, you don't manually send "BEGIN" and "COMMIT" queries. Instead, you use the begin() method on your pool. This gives you a Transaction object that wraps a connection. The most important thing to remember is that if this object is dropped without commit() being called, SQLx will automatically roll back the changes for you. I've seen plenty of bugs where developers forgot to commit, and they spent an hour wondering why their data wasn't saving—it's almost always because the transaction was dropped at the end of the scope.
async fn place_order(pool: &PgPool, user_id: i32, product_id: i32) -> anyhow::Result<()> {
// Start the transaction
let mut tx = pool.begin().await?;
// Use 'tx' instead of 'pool' for your queries
sqlx::query!("UPDATE inventory SET stock = stock - 1 WHERE id = $1", product_id)
.execute(&mut *tx)
.await?;
sqlx::query!("INSERT INTO orders (user_id, product_id) VALUES ($1, $2)", user_id, product_id)
.execute(&mut *tx)
.await?;
// If we reach here, everything succeeded. Now we commit.
tx.commit().await?;
Ok(())
}
How do I pass a transaction into other functions?
This is where things get a bit clunky in Rust. You'll likely want to break your logic into smaller functions, but you can't just pass the pool because that would start a new connection. You need to pass a reference to the transaction. The trick is using the &mut *tx syntax or, more flexibly, using the Executor trait. For most of your projects, simply passing a mutable reference to the transaction is the most straightforward path.
Notice in the example below how I use &mut PgConnection as the argument type. Since Transaction derefs to the connection, this works perfectly and keeps your helper functions clean.
async fn deduct_stock(executor: &mut PgConnection, product_id: i32) -> anyhow::Result<()> {
sqlx::query!("UPDATE inventory SET stock = stock - 1 WHERE id = $1", product_id)
.execute(executor)
.await?;
Ok(())
}
async fn create_order_record(executor: &mut PgConnection, user_id: i32, product_id: i32) -> anyhow::Result<()> {
sqlx::query!("INSERT INTO orders (user_id, product_id) VALUES ($1, $2)", user_id, product_id)
.execute(executor)
.await?;
Ok(())
}
// Then in your main logic:
let mut tx = pool.begin().await?;
deduct_stock(&mut *tx, product_id).await?;
create_order_record(&mut *tx, user_id, product_id).await?;
tx.commit().await?;
Do I need to call rollback manually if an error occurs?
Short answer: No. One of the nicest things about the way SQLx handles transactions is that it leverages Rust's ownership and drop semantics. If your function returns early via the ? operator, the tx variable goes out of scope and is dropped. When a Transaction is dropped without commit() being called, SQLx sends a rollback command to the database.
I usually tell people to think of tx.commit() as the "Save" button. Everything you do up until that line is just a draft. If the code crashes, returns an error, or panics, the draft is simply thrown away. This makes your error handling significantly leaner because you don't have to wrap every single query in a massive try-catch block just to trigger a rollback.
📋 Practical Task
Exercise: Implement an Atomic Fund Transfer between User Accounts
You are building a simple banking module. You need to create a function transfer_funds(pool: &PgPool, from_id: i32, to_id: i32, amount: i64) that ensures money is never "lost" during a transfer.
Requirements:
- Start a transaction using
pool.begin(). - Subtract the
amountfrom the account matchingfrom_id. - Add the
amountto the account matchingto_id. - Ensure that if the second update fails (e.g., the
to_iddoesn't exist), the money is not deducted from the first account. - Commit the transaction only after both operations succeed.
Database Schema for context:
CREATE TABLE accounts (
id SERIAL PRIMARY KEY,
balance BIGINT NOT NULL
);
There are no comments for now.