@starbemtech/star-db-query-builder
Version:
A query builder to be used with mysql or postgres
738 lines (615 loc) • 17 kB
Markdown
# Transactions
Execute multiple database operations within a single transaction to ensure data consistency and atomicity.
## Overview
Transactions ensure that a series of database operations either all succeed or all fail together. This is crucial for maintaining data integrity when performing complex operations that involve multiple tables or records.
## Available Methods
### withTransaction
Executes a function within a database transaction with automatic commit/rollback handling.
```typescript
withTransaction<T>(
dbClient: IDatabaseClient,
transactionFn: (tx: ITransactionClient) => Promise<T>
): Promise<T>
```
### beginTransaction
Creates a transaction client for manual transaction management.
```typescript
beginTransaction(dbClient: IDatabaseClient): Promise<ITransactionClient>
```
## ITransactionClient Interface
```typescript
interface ITransactionClient {
query: <T>(sql: string, params?: any[]) => Promise<T>
commit: () => Promise<void>
rollback: () => Promise<void>
}
```
## Examples
### Basic Transaction with withTransaction
```typescript
import {
withTransaction,
insert,
update,
} from '@starbemtech/star-db-query-builder'
// Create user with profile in a single transaction
const createUserWithProfile = async (userData: any, profileData: any) => {
return withTransaction(dbClient, async (tx) => {
// Create user
const user = await insert({
tableName: 'users',
dbClient: tx,
data: userData,
})
// Create user profile
const profile = await insert({
tableName: 'user_profiles',
dbClient: tx,
data: {
...profileData,
user_id: user.id,
},
})
return { user, profile }
})
}
// Usage
try {
const result = await createUserWithProfile(
{ name: 'John Doe', email: 'john@example.com' },
{ bio: 'Software developer', location: 'New York' }
)
console.log('User and profile created successfully:', result)
} catch (error) {
console.error('Transaction failed:', error.message)
// Both user and profile creation were rolled back
}
```
### E-commerce Order Processing
```typescript
const processOrder = async (orderData: any, orderItems: any[]) => {
return withTransaction(dbClient, async (tx) => {
// Create order
const order = await insert({
tableName: 'orders',
dbClient: tx,
data: {
...orderData,
status: 'pending',
total: 0, // Will be calculated
},
})
let totalAmount = 0
// Create order items and calculate total
for (const item of orderItems) {
const orderItem = await insert({
tableName: 'order_items',
dbClient: tx,
data: {
...item,
order_id: order.id,
},
})
totalAmount += item.price * item.quantity
// Update product stock
await update({
tableName: 'products',
dbClient: tx,
id: item.product_id,
data: {
stock: { operator: '-', value: item.quantity },
},
})
}
// Update order total
await update({
tableName: 'orders',
dbClient: tx,
id: order.id,
data: {
total: totalAmount,
status: 'confirmed',
},
})
return { order, totalAmount }
})
}
```
### Account Transfer
```typescript
const transferMoney = async (
fromAccountId: string,
toAccountId: string,
amount: number
) => {
return withTransaction(dbClient, async (tx) => {
// Check sender balance
const fromAccount = await findFirst({
tableName: 'accounts',
dbClient: tx,
where: { id: { operator: '=', value: fromAccountId } },
})
if (!fromAccount || fromAccount.balance < amount) {
throw new Error('Insufficient funds')
}
// Debit from sender
await update({
tableName: 'accounts',
dbClient: tx,
id: fromAccountId,
data: {
balance: { operator: '-', value: amount },
},
})
// Credit to receiver
await update({
tableName: 'accounts',
dbClient: tx,
id: toAccountId,
data: {
balance: { operator: '+', value: amount },
},
})
// Create transaction record
const transaction = await insert({
tableName: 'transactions',
dbClient: tx,
data: {
from_account_id: fromAccountId,
to_account_id: toAccountId,
amount,
type: 'transfer',
status: 'completed',
},
})
return transaction
})
}
```
### Manual Transaction Management
```typescript
import {
beginTransaction,
insert,
update,
} from '@starbemtech/star-db-query-builder'
const complexOperation = async () => {
const transaction = await beginTransaction(dbClient)
try {
// First operation
const user = await insert({
tableName: 'users',
dbClient: transaction,
data: { name: 'John Doe', email: 'john@example.com' },
})
// Second operation
const profile = await insert({
tableName: 'user_profiles',
dbClient: transaction,
data: { user_id: user.id, bio: 'Hello world' },
})
// Third operation
await update({
tableName: 'users',
dbClient: transaction,
id: user.id,
data: { profile_created: true },
})
// Commit all changes
await transaction.commit()
return { user, profile }
} catch (error) {
// Rollback on any error
await transaction.rollback()
throw error
}
}
```
### Nested Operations with Error Handling
```typescript
const createUserWithMultipleRelations = async (userData: any) => {
return withTransaction(dbClient, async (tx) => {
try {
// Create user
const user = await insert({
tableName: 'users',
dbClient: tx,
data: userData,
})
// Create user preferences
const preferences = await insert({
tableName: 'user_preferences',
dbClient: tx,
data: {
user_id: user.id,
theme: 'dark',
notifications: true,
},
})
// Create user settings
const settings = await insert({
tableName: 'user_settings',
dbClient: tx,
data: {
user_id: user.id,
language: 'en',
timezone: 'UTC',
},
})
// Create audit log
await insert({
tableName: 'audit_logs',
dbClient: tx,
data: {
user_id: user.id,
action: 'user_created',
details: JSON.stringify({ email: user.email }),
},
})
return { user, preferences, settings }
} catch (error) {
// Transaction will be automatically rolled back
console.error('Failed to create user with relations:', error)
throw error
}
})
}
```
### Batch Operations with Transactions
```typescript
const bulkUserCreation = async (usersData: any[]) => {
return withTransaction(dbClient, async (tx) => {
const results = []
for (const userData of usersData) {
// Create user
const user = await insert({
tableName: 'users',
dbClient: tx,
data: userData,
})
// Create default profile
const profile = await insert({
tableName: 'user_profiles',
dbClient: tx,
data: {
user_id: user.id,
bio: 'New user',
created_at: new Date(),
},
})
results.push({ user, profile })
}
// Create batch audit log
await insert({
tableName: 'audit_logs',
dbClient: tx,
data: {
action: 'bulk_user_creation',
details: JSON.stringify({
count: usersData.length,
user_ids: results.map((r) => r.user.id),
}),
},
})
return results
})
}
```
### Conditional Transaction Logic
```typescript
const processPayment = async (paymentData: any) => {
return withTransaction(dbClient, async (tx) => {
// Create payment record
const payment = await insert({
tableName: 'payments',
dbClient: tx,
data: {
...paymentData,
status: 'processing',
},
})
// Check if payment amount is above threshold
if (paymentData.amount > 1000) {
// Require manual approval for large payments
await insert({
tableName: 'payment_approvals',
dbClient: tx,
data: {
payment_id: payment.id,
status: 'pending',
requires_approval: true,
},
})
// Update payment status
await update({
tableName: 'payments',
dbClient: tx,
id: payment.id,
data: { status: 'pending_approval' },
})
} else {
// Auto-approve small payments
await update({
tableName: 'payments',
dbClient: tx,
id: payment.id,
data: { status: 'approved' },
})
// Process the payment
await processApprovedPayment(payment.id, tx)
}
return payment
})
}
const processApprovedPayment = async (
paymentId: string,
tx: ITransactionClient
) => {
// Additional payment processing logic
await update({
tableName: 'payments',
dbClient: tx,
id: paymentId,
data: { status: 'completed', processed_at: new Date() },
})
}
```
## Best Practices
### 1. Keep Transactions Short
```typescript
// Good: Short, focused transaction
const updateUserStatus = async (userId: string, status: string) => {
return withTransaction(dbClient, async (tx) => {
await update({
tableName: 'users',
dbClient: tx,
id: userId,
data: { status },
})
await insert({
tableName: 'user_status_history',
dbClient: tx,
data: { user_id: userId, status, changed_at: new Date() },
})
})
}
// Avoid: Long-running transactions
const badTransaction = async () => {
return withTransaction(dbClient, async (tx) => {
// ... many operations
await someSlowOperation() // This could timeout
// ... more operations
})
}
```
### 2. Handle Errors Properly
```typescript
const safeTransaction = async () => {
try {
return await withTransaction(dbClient, async (tx) => {
// Transaction operations
const result = await someOperation(tx)
return result
})
} catch (error) {
// Transaction was automatically rolled back
console.error('Transaction failed:', error.message)
// Handle specific error types
if (error.message.includes('duplicate key')) {
throw new Error('Record already exists')
}
throw error
}
}
```
### 3. Use Appropriate Isolation Levels
```typescript
// For read-heavy operations, consider using read-only transactions
const getReportData = async () => {
return withTransaction(dbClient, async (tx) => {
// Set transaction to read-only (database-specific)
await tx.query('SET TRANSACTION READ ONLY')
const users = await findMany({
tableName: 'users',
dbClient: tx,
where: { status: { operator: '=', value: 'active' } },
})
const orders = await findMany({
tableName: 'orders',
dbClient: tx,
where: { status: { operator: '=', value: 'completed' } },
})
return { users, orders }
})
}
```
### 4. Avoid Nested Transactions
```typescript
// Good: Single transaction for related operations
const createOrderWithItems = async (orderData: any, items: any[]) => {
return withTransaction(dbClient, async (tx) => {
const order = await insert({
tableName: 'orders',
dbClient: tx,
data: orderData,
})
for (const item of items) {
await insert({
tableName: 'order_items',
dbClient: tx,
data: { ...item, order_id: order.id },
})
}
return order
})
}
// Avoid: Nested transactions (not supported by most databases)
const badNestedTransaction = async () => {
return withTransaction(dbClient, async (tx1) => {
// ... operations
return withTransaction(dbClient, async (tx2) => {
// This won't work as expected
})
})
}
```
## Error Handling
### Common Transaction Errors
```typescript
const handleTransactionErrors = async () => {
try {
return await withTransaction(dbClient, async (tx) => {
// Your transaction logic
})
} catch (error) {
if (error.message.includes('deadlock detected')) {
// Handle deadlock - you might want to retry
console.warn('Deadlock detected, retrying...')
// Implement retry logic
} else if (error.message.includes('serialization failure')) {
// Handle serialization failure
console.warn('Serialization failure, retrying...')
// Implement retry logic
} else if (error.message.includes('connection lost')) {
// Handle connection issues
console.error('Database connection lost')
// Implement reconnection logic
} else {
// Handle other errors
console.error('Transaction error:', error.message)
}
throw error
}
}
```
### Retry Logic for Transient Errors
```typescript
const retryTransaction = async <T>(
transactionFn: (tx: ITransactionClient) => Promise<T>,
maxRetries: number = 3
): Promise<T> => {
let lastError: Error
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await withTransaction(dbClient, transactionFn)
} catch (error) {
lastError = error as Error
// Check if error is retryable
if (isRetryableError(error) && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000 // Exponential backoff
console.warn(
`Transaction attempt ${attempt} failed, retrying in ${delay}ms...`
)
await new Promise((resolve) => setTimeout(resolve, delay))
continue
}
throw error
}
}
throw lastError!
}
const isRetryableError = (error: any): boolean => {
const retryableErrors = [
'deadlock detected',
'serialization failure',
'connection lost',
'timeout',
]
return retryableErrors.some((msg) =>
error.message?.toLowerCase().includes(msg)
)
}
```
## Performance Considerations
### 1. Connection Pooling
```typescript
// Ensure your database client is configured with proper connection pooling
await initDb({
type: 'pg',
options: {
host: 'localhost',
port: 5432,
database: 'myapp',
user: 'username',
password: 'password',
max: 20, // Maximum connections in pool
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
},
})
```
### 2. Transaction Timeout
```typescript
// Set appropriate timeouts for transactions
const quickTransaction = async () => {
return withTransaction(dbClient, async (tx) => {
// Set a timeout for this transaction
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Transaction timeout')), 5000)
})
const transactionPromise = (async () => {
// Your transaction logic
return await someOperation(tx)
})()
return Promise.race([transactionPromise, timeoutPromise])
})
}
```
### 3. Batch Operations
```typescript
// For large batch operations, consider processing in chunks
const bulkUpdateWithTransactions = async (
records: any[],
chunkSize: number = 100
) => {
const results = []
for (let i = 0; i < records.length; i += chunkSize) {
const chunk = records.slice(i, i + chunkSize)
const chunkResult = await withTransaction(dbClient, async (tx) => {
const chunkResults = []
for (const record of chunk) {
const result = await update({
tableName: 'records',
dbClient: tx,
id: record.id,
data: record.data,
})
chunkResults.push(result)
}
return chunkResults
})
results.push(...chunkResult)
}
return results
}
```
## Database-Specific Considerations
### PostgreSQL
- Supports nested transactions (savepoints)
- Has excellent transaction isolation
- Supports advisory locks for complex scenarios
### MySQL
- Uses autocommit mode by default
- Supports different isolation levels
- Has limitations with nested transactions
## Monitoring and Logging
```typescript
// Monitor transaction events
import { monitor, MonitorEvents } from '@starbemtech/star-db-query-builder'
monitor.on(MonitorEvents.TRANSACTION_COMMIT, (data) => {
console.log('Transaction committed:', data)
})
monitor.on(MonitorEvents.TRANSACTION_ROLLBACK, (data) => {
console.log('Transaction rolled back:', data)
})
monitor.on(MonitorEvents.QUERY_START, (data) => {
if (data.inTransaction) {
console.log('Query in transaction:', data.sql)
}
})
```
## Summary
Transactions are essential for maintaining data consistency in complex operations. The library provides two main approaches:
1. **`withTransaction`**: Automatic transaction management with commit/rollback
2. **`beginTransaction`**: Manual transaction control for advanced scenarios
Always handle errors properly and keep transactions as short as possible to avoid performance issues and deadlocks.