Relations and Transactions

Nested Writes

Create an order with its items in one call. Prisma runs it in a transaction automatically.

const order = await this.prisma.order.create({
  data: {
    user: { connect: { id: userId } },
    total: new Prisma.Decimal('2598.00'),
    items: {
      create: [
        { product: { connect: { id: 7 } }, quantity: 2, unitPrice: new Prisma.Decimal('1299.00') },
      ],
    },
  },
  include: { items: { include: { product: { select: { name: true } } } } },
});

Interactive Transactions

When later steps depend on earlier reads, use $transaction(async (tx) => ...). Everything inside uses tx; a thrown error rolls back.

src/orders/orders.service.ts

import { Injectable, UnprocessableEntityException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class OrdersService {
  constructor(private readonly prisma: PrismaService) {}

  place(userId: number, items: { productId: number; quantity: number }[]) {
    return this.prisma.$transaction(async (tx) => {
      let total = new Prisma.Decimal(0);
      const lines: Prisma.OrderItemCreateWithoutOrderInput[] = [];

      for (const { productId, quantity } of items) {
        const { count } = await tx.product.updateMany({
          where: { id: productId, stock: { gte: quantity } },
          data: { stock: { decrement: quantity } },
        });
        if (count === 0) throw new UnprocessableEntityException(`Product ${productId} is out of stock`);
        const product = await tx.product.findUniqueOrThrow({ where: { id: productId } });
        total = total.add(product.price.mul(quantity));
        lines.push({ product: { connect: { id: productId } }, quantity, unitPrice: product.price });
      }

      return tx.order.create({
        data: { userId, total, items: { create: lines } },
        include: { items: true },
      });
    });
  }
}

Example

Why the where stock >= quantity trick matters: two customers buy the last unit at the same time. A "read, check, then write" version oversells; the atomic conditional update does not.

let stock = 1;

async function naiveBuy(name: string) {
  const current = stock;                          // read
  await new Promise((r) => setTimeout(r, 10));    // other request runs here
  if (current >= 1) { stock = current - 1; return name + ': bought (stock now ' + stock + ')'; }
  return name + ': out of stock';
}

function atomicBuy(name: string) {
  // like UPDATE products SET stock = stock - 1 WHERE id = 7 AND stock >= 1
  const count = stock >= 1 ? (stock -= 1, 1) : 0;
  return name + (count ? ': bought (stock now ' + stock + ')' : ': out of stock');
}

async function main() {
  console.log('naive:', await Promise.all([naiveBuy('Asha'), naiveBuy('Ravi')]));
  console.log('units sold from 1 in stock: 2 (oversold!)');
  stock = 1;
  console.log('atomic:', [atomicBuy('Asha'), atomicBuy('Ravi')]);
}
main();

Output

naive: [ 'Asha: bought (stock now 0)', 'Ravi: bought (stock now 0)' ]
units sold from 1 in stock: 2 (oversold!)
atomic: [ 'Asha: bought (stock now 0)', 'Ravi: out of stock' ]