210 lines
5.4 KiB
Markdown
210 lines
5.4 KiB
Markdown
# 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
|
|
|
|
```rust
|
|
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
|
|
|
|
1. **Automatic commit**: Transaction commits if closure returns `Ok`
|
|
2. **Automatic rollback**: Transaction rolls back if closure returns `Err` or panics
|
|
3. **Return values**: The closure can return any value wrapped in `Result<T, sqlx::Error>`
|
|
4. **Executor access**: Use `&mut *tx` to pass the transaction as an executor
|
|
|
|
## Usage Examples
|
|
|
|
### Basic Transaction
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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
|
|
|
|
```rust
|
|
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)
|
|
|
|
```rust
|
|
// 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:
|
|
|
|
```rust
|
|
// 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
|
|
|
|
```rust
|
|
// 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 `.await` for all database operations
|