Saas.js logo
Drizzle CRUD/Advanced

Transactions

Use database transactions with Drizzle CRUD operations

Transactions

Drizzle CRUD supports database transactions by allowing you to pass a transaction instance through the operation context. This ensures data consistency across multiple operations.

Basic Transaction Usage

Pass a transaction instance to any CRUD operation:

import { drizzle } from 'drizzle-orm/postgres-js'

const db = drizzle(/* connection */)

const result = await db.transaction(async (tx) => {
  // Create user
  const user = await userCrud.create(
    {
      name: 'John Doe',
      email: 'john@example.com',
    },
    {
      db: tx, // Pass transaction instance
    }
  )

  // Create user profile
  const profile = await profileCrud.create(
    {
      userId: user.id,
      bio: 'Software developer',
    },
    {
      db: tx,
    }
  )

  return { user, profile }
})

Multiple Operations in Transaction

Perform multiple CRUD operations within a single transaction:

const transferFunds = async (fromUserId: string, toUserId: string, amount: number) => {
  return await db.transaction(async (tx) => {
    // Deduct from sender
    const fromUser = await userCrud.findById(fromUserId, { db: tx })
    if (!fromUser || fromUser.balance < amount) {
      throw new Error('Insufficient funds')
    }

    await userCrud.update(
      fromUserId,
      { balance: fromUser.balance - amount },
      { db: tx }
    )

    // Add to receiver
    const toUser = await userCrud.findById(toUserId, { db: tx })
    if (!toUser) {
      throw new Error('Recipient not found')
    }

    await userCrud.update(
      toUserId,
      { balance: toUser.balance + amount },
      { db: tx }
    )

    // Create transaction record
    const transaction = await transactionCrud.create(
      {
        fromUserId,
        toUserId,
        amount,
        type: 'transfer',
        status: 'completed',
      },
      { db: tx }
    )

    return transaction
  })
}

Error Handling in Transactions

Transactions automatically roll back on errors:

const createOrderWithItems = async (orderData: any, items: any[]) => {
  try {
    return await db.transaction(async (tx) => {
      // Create order
      const order = await orderCrud.create(orderData, { db: tx })

      // Create order items
      const orderItems = []
      for (const item of items) {
        const orderItem = await orderItemCrud.create(
          {
            orderId: order.id,
            productId: item.productId,
            quantity: item.quantity,
            price: item.price,
          },
          { db: tx }
        )
        orderItems.push(orderItem)
      }

      // Update inventory
      for (const item of items) {
        const product = await productCrud.findById(item.productId, { db: tx })
        if (product.stock < item.quantity) {
          throw new Error(`Insufficient stock for product ${item.productId}`)
        }

        await productCrud.update(
          item.productId,
          { stock: product.stock - item.quantity },
          { db: tx }
        )
      }

      return { order, orderItems }
    })
  } catch (error) {
    console.error('Order creation failed:', error)
    throw error
  }
}

Nested Transactions

Drizzle supports nested transactions (savepoints):

const complexOperation = async () => {
  return await db.transaction(async (tx) => {
    // Main transaction
    const user = await userCrud.create(
      { name: 'John', email: 'john@example.com' },
      { db: tx }
    )

    try {
      // Nested transaction (savepoint)
      await tx.transaction(async (nestedTx) => {
        await profileCrud.create(
          { userId: user.id, bio: 'Developer' },
          { db: nestedTx }
        )

        // This might fail
        await riskyCrud.create(
          { userId: user.id, data: 'risky' },
          { db: nestedTx }
        )
      })
    } catch (error) {
      // Nested transaction rolls back, main transaction continues
      console.log('Profile creation failed, continuing...')
    }

    // Create settings (always succeeds)
    await settingsCrud.create(
      { userId: user.id, theme: 'dark' },
      { db: tx }
    )

    return user
  })
}

Transaction with Hooks

Hooks work within transactions:

const userCrud = createCrud(users, {
  hooks: {
    afterCreate: async (user, { context }) => {
      // This runs within the same transaction
      if (context?.db) {
        await auditCrud.create(
          {
            operation: 'create',
            tableName: 'users',
            recordId: user.id,
            timestamp: new Date(),
          },
          { db: context.db }
        )
      }
      
      return user
    },
  },
})

// Usage
await db.transaction(async (tx) => {
  // Both user creation and audit log creation happen in same transaction
  const user = await userCrud.create(
    { name: 'John', email: 'john@example.com' },
    { db: tx }
  )
})

Transaction Isolation Levels

Configure transaction isolation levels:

// PostgreSQL example
const result = await db.transaction(
  async (tx) => {
    const user = await userCrud.create(
      { name: 'John', email: 'john@example.com' },
      { db: tx }
    )
    
    return user
  },
  {
    isolationLevel: 'serializable',
  }
)

Long-Running Transactions

For long-running transactions, consider chunking operations:

const bulkImport = async (records: any[]) => {
  const chunkSize = 1000
  const chunks = []
  
  for (let i = 0; i < records.length; i += chunkSize) {
    chunks.push(records.slice(i, i + chunkSize))
  }

  const results = []
  
  for (const chunk of chunks) {
    const chunkResult = await db.transaction(async (tx) => {
      const created = []
      
      for (const record of chunk) {
        const created_record = await userCrud.create(record, { db: tx })
        created.push(created_record)
      }
      
      return created
    })
    
    results.push(...chunkResult)
  }
  
  return results
}

Transaction Best Practices

1. Keep Transactions Short

// Good: Short transaction
const quickTransaction = async () => {
  return await db.transaction(async (tx) => {
    const user = await userCrud.create(userData, { db: tx })
    await auditCrud.create(auditData, { db: tx })
    return user
  })
}

// Avoid: Long-running transaction
const longTransaction = async () => {
  return await db.transaction(async (tx) => {
    const user = await userCrud.create(userData, { db: tx })
    
    // Don't do heavy processing in transactions
    await sendEmailsToAllUsers() // This takes too long
    await generateReports() // This takes too long
    
    return user
  })
}

2. Handle Deadlocks

const retryTransaction = async (operation: () => Promise<any>, maxRetries = 3) => {
  let attempt = 0
  
  while (attempt < maxRetries) {
    try {
      return await operation()
    } catch (error) {
      if (error.code === '40001' && attempt < maxRetries - 1) {
        // Deadlock detected, retry
        attempt++
        await new Promise(resolve => setTimeout(resolve, 100 * attempt))
        continue
      }
      throw error
    }
  }
}

// Usage
const result = await retryTransaction(async () => {
  return await db.transaction(async (tx) => {
    await userCrud.update(userId1, { balance: newBalance1 }, { db: tx })
    await userCrud.update(userId2, { balance: newBalance2 }, { db: tx })
  })
})

3. Use Consistent Transaction Context

// Good: Consistent transaction passing
const createUserWithProfile = async (userData: any, profileData: any) => {
  return await db.transaction(async (tx) => {
    const context = { db: tx }
    
    const user = await userCrud.create(userData, context)
    const profile = await profileCrud.create(
      { ...profileData, userId: user.id },
      context
    )
    
    return { user, profile }
  })
}

// Avoid: Inconsistent transaction usage
const inconsistentTransaction = async () => {
  return await db.transaction(async (tx) => {
    const user = await userCrud.create(userData, { db: tx })
    
    // This won't be in the transaction!
    const profile = await profileCrud.create(profileData)
    
    return { user, profile }
  })
}

Advanced Transaction Patterns

Saga Pattern

Handle complex business transactions across multiple services:

const processOrder = async (orderData: any) => {
  const saga = {
    actions: [],
    compensations: [],
  }

  try {
    // Step 1: Create order
    const order = await db.transaction(async (tx) => {
      const order = await orderCrud.create(orderData, { db: tx })
      saga.actions.push({ type: 'order_created', orderId: order.id })
      saga.compensations.unshift({
        type: 'cancel_order',
        orderId: order.id,
      })
      return order
    })

    // Step 2: Reserve inventory
    await db.transaction(async (tx) => {
      await inventoryCrud.update(
        orderData.productId,
        { reserved: { increment: orderData.quantity } },
        { db: tx }
      )
      saga.actions.push({ type: 'inventory_reserved' })
      saga.compensations.unshift({ type: 'release_inventory' })
    })

    // Step 3: Process payment
    await db.transaction(async (tx) => {
      await paymentCrud.create(
        {
          orderId: order.id,
          amount: orderData.total,
          status: 'processed',
        },
        { db: tx }
      )
      saga.actions.push({ type: 'payment_processed' })
    })

    return order
  } catch (error) {
    // Compensate for completed actions
    for (const compensation of saga.compensations) {
      await executeCompensation(compensation)
    }
    throw error
  }
}

Next Steps