Initial commit
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
# Complete Examples
|
||||
|
||||
## Example 1: Banking Transfer
|
||||
|
||||
**Input:** Transfer money between accounts with strict consistency.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
async function transferMoney(
|
||||
fromAccountId: string,
|
||||
toAccountId: string,
|
||||
amount: number
|
||||
) {
|
||||
if (amount <= 0) {
|
||||
throw new Error('Amount must be positive');
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const fromAccount = await tx.account.findUnique({
|
||||
where: { id: fromAccountId }
|
||||
});
|
||||
|
||||
if (!fromAccount) {
|
||||
throw new Error('Source account not found');
|
||||
}
|
||||
|
||||
if (fromAccount.balance < amount) {
|
||||
throw new Error('Insufficient funds');
|
||||
}
|
||||
|
||||
const toAccount = await tx.account.findUnique({
|
||||
where: { id: toAccountId }
|
||||
});
|
||||
|
||||
if (!toAccount) {
|
||||
throw new Error('Destination account not found');
|
||||
}
|
||||
|
||||
await tx.account.update({
|
||||
where: { id: fromAccountId },
|
||||
data: { balance: { decrement: amount } }
|
||||
});
|
||||
|
||||
await tx.account.update({
|
||||
where: { id: toAccountId },
|
||||
data: { balance: { increment: amount } }
|
||||
});
|
||||
|
||||
const transfer = await tx.transfer.create({
|
||||
data: {
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
amount,
|
||||
status: 'COMPLETED',
|
||||
completedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return transfer;
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
||||
maxWait: 5000,
|
||||
timeout: 10000
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true, transfer: result };
|
||||
|
||||
} catch (error) {
|
||||
if (error.code === 'P2034') {
|
||||
throw new Error('Transaction conflict - please retry');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example 2: Inventory Reservation
|
||||
|
||||
**Input:** Reserve inventory items for an order.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
async function reserveInventory(
|
||||
orderId: string,
|
||||
items: Array<{ productId: string; quantity: number }>
|
||||
) {
|
||||
const maxRetries = 3;
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
try {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
for (const item of items) {
|
||||
const product = await tx.product.findUnique({
|
||||
where: { id: item.productId }
|
||||
});
|
||||
|
||||
if (!product) {
|
||||
throw new Error(`Product ${item.productId} not found`);
|
||||
}
|
||||
|
||||
if (product.stock < item.quantity) {
|
||||
throw new Error(
|
||||
`Insufficient stock for ${product.name}`
|
||||
);
|
||||
}
|
||||
|
||||
await tx.product.update({
|
||||
where: { id: item.productId },
|
||||
data: { stock: { decrement: item.quantity } }
|
||||
});
|
||||
|
||||
await tx.reservation.create({
|
||||
data: {
|
||||
orderId,
|
||||
productId: item.productId,
|
||||
quantity: item.quantity,
|
||||
reservedAt: new Date()
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
|
||||
maxWait: 3000,
|
||||
timeout: 8000
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
if (error.code === 'P2034' && attempt < maxRetries - 1) {
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, Math.pow(2, attempt) * 200)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Reservation failed after retries');
|
||||
}
|
||||
```
|
||||
|
||||
## Example 3: Seat Booking with Status Check
|
||||
|
||||
**Input:** Book a seat with concurrent user protection.
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```typescript
|
||||
async function bookSeat(
|
||||
userId: string,
|
||||
eventId: string,
|
||||
seatNumber: string
|
||||
) {
|
||||
try {
|
||||
const booking = await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const seat = await tx.seat.findFirst({
|
||||
where: {
|
||||
eventId,
|
||||
seatNumber
|
||||
}
|
||||
});
|
||||
|
||||
if (!seat) {
|
||||
throw new Error('Seat not found');
|
||||
}
|
||||
|
||||
if (seat.status !== 'AVAILABLE') {
|
||||
throw new Error('Seat is no longer available');
|
||||
}
|
||||
|
||||
await tx.seat.update({
|
||||
where: { id: seat.id },
|
||||
data: {
|
||||
status: 'BOOKED',
|
||||
bookedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
const booking = await tx.booking.create({
|
||||
data: {
|
||||
userId,
|
||||
seatId: seat.id,
|
||||
eventId,
|
||||
status: 'CONFIRMED',
|
||||
bookedAt: new Date()
|
||||
}
|
||||
});
|
||||
|
||||
return booking;
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true, booking };
|
||||
|
||||
} catch (error) {
|
||||
if (error.code === 'P2034') {
|
||||
throw new Error(
|
||||
'Seat was just booked by another user - please select another seat'
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,63 @@
|
||||
# Database-Specific Defaults
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Default: `ReadCommitted`
|
||||
|
||||
Supported levels:
|
||||
|
||||
- `Serializable` (strictest)
|
||||
- `RepeatableRead`
|
||||
- `ReadCommitted` (default)
|
||||
|
||||
Notes:
|
||||
|
||||
- PostgreSQL uses true Serializable isolation (not snapshot isolation)
|
||||
- May throw serialization errors under high concurrency
|
||||
- Excellent MVCC implementation reduces conflicts
|
||||
|
||||
## MySQL
|
||||
|
||||
Default: `RepeatableRead`
|
||||
|
||||
Supported levels:
|
||||
|
||||
- `Serializable`
|
||||
- `RepeatableRead` (default)
|
||||
- `ReadCommitted`
|
||||
- `ReadUncommitted` (not recommended)
|
||||
|
||||
Notes:
|
||||
|
||||
- InnoDB engine required for transaction support
|
||||
- Uses gap locking in RepeatableRead mode
|
||||
- Serializable adds locking to SELECT statements
|
||||
|
||||
## SQLite
|
||||
|
||||
Default: `Serializable`
|
||||
|
||||
Supported levels:
|
||||
|
||||
- `Serializable` (only level - database-wide lock)
|
||||
|
||||
Notes:
|
||||
|
||||
- Only one writer at a time
|
||||
- No true isolation level configuration
|
||||
- Best for single-user or low-concurrency applications
|
||||
|
||||
## MongoDB
|
||||
|
||||
Default: `Snapshot` (similar to RepeatableRead)
|
||||
|
||||
Supported levels:
|
||||
|
||||
- `Snapshot` (equivalent to RepeatableRead)
|
||||
- `Majority` read concern
|
||||
|
||||
Notes:
|
||||
|
||||
- Different isolation model than SQL databases
|
||||
- Uses write-ahead log for consistency
|
||||
- Replica set required for transactions
|
||||
@@ -0,0 +1,124 @@
|
||||
# Preventing Race Conditions
|
||||
|
||||
## Lost Update Problem
|
||||
|
||||
**Scenario:** Two transactions read the same value, both update it, one overwrites the other.
|
||||
|
||||
**Without Isolation:**
|
||||
|
||||
```typescript
|
||||
const product = await prisma.product.findUnique({
|
||||
where: { id: productId }
|
||||
});
|
||||
|
||||
await prisma.product.update({
|
||||
where: { id: productId },
|
||||
data: { stock: product.stock - quantity }
|
||||
});
|
||||
```
|
||||
|
||||
Transaction A reads stock: 10
|
||||
Transaction B reads stock: 10
|
||||
Transaction A writes stock: 5 (10 - 5)
|
||||
Transaction B writes stock: 8 (10 - 2)
|
||||
Result: Stock is 8, but should be 3
|
||||
|
||||
**With Serializable Isolation:**
|
||||
|
||||
```typescript
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const product = await tx.product.findUnique({
|
||||
where: { id: productId }
|
||||
});
|
||||
|
||||
if (product.stock < quantity) {
|
||||
throw new Error('Insufficient stock');
|
||||
}
|
||||
|
||||
await tx.product.update({
|
||||
where: { id: productId },
|
||||
data: { stock: { decrement: quantity } }
|
||||
});
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
One transaction succeeds, the other gets P2034 and retries with fresh data.
|
||||
|
||||
## Double-Booking Problem
|
||||
|
||||
**Scenario:** Two users try to book the same resource simultaneously.
|
||||
|
||||
**Solution:**
|
||||
|
||||
```typescript
|
||||
async function bookSeat(userId: string, seatId: string) {
|
||||
try {
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const seat = await tx.seat.findUnique({
|
||||
where: { id: seatId }
|
||||
});
|
||||
|
||||
if (seat.status !== 'AVAILABLE') {
|
||||
throw new Error('Seat no longer available');
|
||||
}
|
||||
|
||||
await tx.seat.update({
|
||||
where: { id: seatId },
|
||||
data: {
|
||||
status: 'BOOKED',
|
||||
userId,
|
||||
bookedAt: new Date()
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.Serializable
|
||||
}
|
||||
);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
if (error.code === 'P2034') {
|
||||
throw new Error('Seat was just booked by another user');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Phantom Read Problem
|
||||
|
||||
**Scenario:** Query for rows matching a condition, insert happens, re-query shows different results.
|
||||
|
||||
**Example with RepeatableRead:**
|
||||
|
||||
```typescript
|
||||
await prisma.$transaction(
|
||||
async (tx) => {
|
||||
const activeUsers = await tx.user.findMany({
|
||||
where: { status: 'ACTIVE' }
|
||||
});
|
||||
|
||||
const count = activeUsers.length;
|
||||
|
||||
await tx.report.create({
|
||||
data: {
|
||||
type: 'USER_COUNT',
|
||||
value: count,
|
||||
timestamp: new Date()
|
||||
}
|
||||
});
|
||||
},
|
||||
{
|
||||
isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
RepeatableRead prevents other transactions from changing existing rows, but may still allow new inserts (phantom reads) depending on database implementation.
|
||||
Reference in New Issue
Block a user