PrismaMiddleTechnical

Какие isolation levels и transaction options поддерживает Prisma и когда они важны?

Prisma поддерживает явное указание isolation level для интерактивных транзакций через параметр isolationLevel. Доступны ReadUncommitted, ReadCommitted, RepeatableRead, Snapshot и Serializable — набор зависит от провайдера БД.

Уровни изоляции в Prisma

Prisma позволяет задавать уровень изоляции только для интерактивных транзакций ($transaction с callback). Batch-транзакции используют уровень изоляции по умолчанию провайдера.

Доступные уровни изоляции

  • ReadUncommitted — видит незафиксированные изменения других транзакций (dirty read). PostgreSQL игнорирует этот уровень, повышая до ReadCommitted.
  • ReadCommitted — видит только зафиксированные данные. Default для PostgreSQL и SQL Server.
  • RepeatableRead — повторное чтение тех же строк даёт одинаковый результат. Защищает от non-repeatable read. Default для MySQL InnoDB.
  • Snapshot — только SQL Server. Читает из снимка на начало транзакции.
  • Serializable — полная сериализуемость, максимальная защита. Высокая вероятность deadlock/retry.

Пример использования

import { Prisma } from '@prisma/client';

// Перевод средств — нужен Serializable для защиты от race condition
async function transferFunds(
  fromId: number,
  toId: number,
  amount: number
) {
  return prisma.$transaction(
    async (tx) => {
      const from = await tx.account.findUniqueOrThrow({
        where: { id: fromId },
        select: { balance: true },
      });

      if (from.balance < amount) {
        throw new Error('Insufficient balance');
      }

      const [updatedFrom, updatedTo] = await Promise.all([
        tx.account.update({
          where: { id: fromId },
          data: { balance: { decrement: amount } },
        }),
        tx.account.update({
          where: { id: toId },
          data: { balance: { increment: amount } },
        }),
      ]);

      return { from: updatedFrom, to: updatedTo };
    },
    {
      isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
      maxWait: 5000,  // мс ожидания соединения из пула
      timeout: 10000, // мс максимального времени транзакции
    }
  );
}

Когда какой уровень использовать

  • ReadCommitted — большинство CRUD-операций, чтение агрегатов без строгих требований к консистентности.
  • RepeatableRead — отчёты, где нужно гарантировать неизменность прочитанных данных в рамках одной транзакции.
  • Serializable — финансовые операции, инвентаризация (списание остатков), любые операции типа «прочитал-проверил-записал».

Retry при Serializable

Serializable-транзакции могут завершаться ошибкой P2034 (serialization failure) — PostgreSQL откатывает транзакцию при конфликте. Нужен retry-механизм:

async function withRetry<T>(
  fn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2034' &&
        attempt < maxRetries - 1
      ) {
        // Exponential backoff
        await new Promise(r => setTimeout(r, 50 * 2 ** attempt));
        continue;
      }
      throw e;
    }
  }
  throw new Error('Max retries exceeded');
}

// Использование
const result = await withRetry(() => transferFunds(1, 2, 100));

Подводные камни

  • PostgreSQL не поддерживает ReadUncommitted — тихо повышает до ReadCommitted, что может скрыть баги при тестировании на MySQL.
  • Serializable в PostgreSQL использует SSI (Serializable Snapshot Isolation) — это не блокировки, а оптимистичная проверка. Высокая конкуренция = частые откаты P2034.
  • Уровень изоляции нельзя задать для batch-транзакций ($transaction([op1, op2])) — только для callback-формы.
  • Длинные Serializable-транзакции с HTTP-запросами внутри — антипаттерн: держат снимки данных и конфликтуют с конкурентными транзакциями.
  • MySQL в RepeatableRead по умолчанию + MVCC может вести себя иначе, чем PostgreSQL — тесты должны запускаться на той же СУБД, что используется в production.
  • Timeout транзакции (timeout опция) — это таймаут Prisma, не PostgreSQL. statement_timeout и lock_timeout нужно настраивать отдельно через $executeRaw.

Common mistakes

  • Отвечать определением без production-сценария.
  • Не называть runtime boundary, security boundary или failure mode.
  • Игнорировать версию API, observability и тестовую проверку.

What the interviewer is testing

  • Объясняет механизм своими словами и без выдуманных API.
  • Называет реальные риски, диагностику и критерий корректности.
  • Связывает ответ с текущей документацией и миграционными ограничениями.

Sources

Related topics