5.4 KiB
5.4 KiB
sqlx-record Transaction Skill
Guide to the transaction! macro for ergonomic transactions.
Triggers
- "transaction", "transactions"
- "commit", "rollback"
- "atomic", "transactional"
Overview
The transaction! macro provides ergonomic transaction handling with automatic commit on success and rollback on error.
Basic Syntax
use sqlx_record::transaction;
let result = transaction!(&pool, |tx| {
// Operations using &mut *tx as executor
user.insert(&mut *tx).await?;
order.insert(&mut *tx).await?;
Ok::<_, sqlx::Error>(order.id) // Return value type annotation
}).await?;
Key Points
- Automatic commit: Transaction commits if closure returns
Ok - Automatic rollback: Transaction rolls back if closure returns
Error panics - Return values: The closure can return any value wrapped in
Result<T, sqlx::Error> - Executor access: Use
&mut *txto pass the transaction as an executor
Usage Examples
Basic Transaction
use sqlx_record::{transaction, prelude::*};
async fn create_user_with_profile(pool: &Pool, user: User, profile: Profile) -> Result<Uuid, sqlx::Error> {
transaction!(&pool, |tx| {
let user_id = user.insert(&mut *tx).await?;
let mut profile = profile;
profile.user_id = user_id;
profile.insert(&mut *tx).await?;
Ok::<_, sqlx::Error>(user_id)
}).await
}
Multiple Operations
async fn transfer_funds(
pool: &Pool,
from_id: &Uuid,
to_id: &Uuid,
amount: i64
) -> Result<(), sqlx::Error> {
transaction!(&pool, |tx| {
// Debit from source
Account::update_by_id(&mut *tx, from_id,
Account::update_form().eval_balance(UpdateExpr::Sub(amount.into()))
).await?;
// Credit to destination
Account::update_by_id(&mut *tx, to_id,
Account::update_form().eval_balance(UpdateExpr::Add(amount.into()))
).await?;
// Create transfer record
let transfer = Transfer {
id: new_uuid(),
from_account: *from_id,
to_account: *to_id,
amount,
created_at: chrono::Utc::now().timestamp_millis(),
};
transfer.insert(&mut *tx).await?;
Ok::<_, sqlx::Error>(())
}).await
}
With Error Handling
async fn create_order(pool: &Pool, cart: Cart) -> Result<Order, AppError> {
transaction!(&pool, |tx| {
// Verify stock
for item in &cart.items {
let product = Product::get_by_id(&mut *tx, &item.product_id).await?
.ok_or(sqlx::Error::RowNotFound)?;
if product.stock < item.quantity {
return Err(sqlx::Error::Protocol("Insufficient stock".into()));
}
}
// Create order
let order = Order {
id: new_uuid(),
user_id: cart.user_id,
status: OrderStatus::PENDING.into(),
total: cart.total(),
created_at: chrono::Utc::now().timestamp_millis(),
};
order.insert(&mut *tx).await?;
// Create order items and decrement stock
for item in cart.items {
let order_item = OrderItem {
id: new_uuid(),
order_id: order.id,
product_id: item.product_id,
quantity: item.quantity,
price: item.price,
};
order_item.insert(&mut *tx).await?;
Product::update_by_id(&mut *tx, &item.product_id,
Product::update_form().eval_stock(UpdateExpr::Sub(item.quantity.into()))
).await?;
}
Ok::<_, sqlx::Error>(order)
}).await.map_err(AppError::from)
}
Nested Operations (Not Nested Transactions)
// Helper function that accepts any executor
async fn create_audit_log<'a, E>(executor: E, action: &str, entity_id: Uuid) -> Result<(), sqlx::Error>
where
E: sqlx::Executor<'a, Database = sqlx::MySql>,
{
let log = AuditLog {
id: new_uuid(),
action: action.into(),
entity_id,
created_at: chrono::Utc::now().timestamp_millis(),
};
log.insert(executor).await?;
Ok(())
}
// Use in transaction
transaction!(&pool, |tx| {
user.insert(&mut *tx).await?;
create_audit_log(&mut *tx, "user_created", user.id).await?;
Ok::<_, sqlx::Error>(())
}).await?;
Type Annotation
The closure must have an explicit return type annotation:
// Correct - with type annotation
Ok::<_, sqlx::Error>(value)
// Also correct
Ok::<i32, sqlx::Error>(42)
// Incorrect - missing annotation (won't compile)
// Ok(value)
Comparison with Manual Transactions
// Manual approach
let mut tx = pool.begin().await?;
match async {
user.insert(&mut *tx).await?;
order.insert(&mut *tx).await?;
Ok::<_, sqlx::Error>(order.id)
}.await {
Ok(result) => {
tx.commit().await?;
Ok(result)
}
Err(e) => {
tx.rollback().await?;
Err(e)
}
}
// With transaction! macro - cleaner
transaction!(&pool, |tx| {
user.insert(&mut *tx).await?;
order.insert(&mut *tx).await?;
Ok::<_, sqlx::Error>(order.id)
}).await
Notes
- The macro works with all supported databases (MySQL, PostgreSQL, SQLite)
- Transactions use the pool's default isolation level
- For custom isolation levels, use sqlx's native transaction API
- The closure is async - use
.awaitfor all database operations