Skip to content

[태호] prisma transaction 공식문서 정리

BaeJ1H021 edited this page Dec 11, 2023 · 1 revision

https://www.prisma.io/docs/guides/performance-and-optimization/prisma-client-transactions-guide

prisma client는 3개의 시나리오에 대해 트랜잭션을 다루는 6개의 다른 방법을 지원함

3개의 시나리오

  • dependent writes
  • independent writes
  • read, modify, write

dependent writes

nested writes

  • user를 생성 후 2개의 게시글과 user를 연결하는 경우 (one-to-many)

  • team을 생성 후 member를 team에 연결하는 경우 (many-to-many)

  • 예제 코드

    const nestedWrite = await prisma.user.create({
      data: {
        email: '[email protected]',
        posts: {
          create: [
            { title: 'My first day at Prisma' },
            { title: 'How to configure a unique constraint in PostgreSQL' },
          ],
        },
      },
    })
  • 예제 코드에서 어느 한 부분이라도 실행에 실패하면 transaction에 대해 roll back함

추천 사용처

  • id와 의존관계를 맺고 있는 record를 다수 생성하는 경우
  • id를 update하고 의존관계를 맺고 있는 record를 생성하는 경우

nested writes와 $transaction([]) API를 동시에 사용하는 예시

  • 2개의 team을 생성하고 user를 할당하는 경우

  • 1개의 team을 생성 후 user를 할당하는 operation을 2회 반복하면 됨

  • 예제 코드

    // Nested write
    const createOne = prisma.team.create({
      data: {
        name: 'Aurora Adventures',
        members: {
          create: {
            email: '[email protected]',
          },
        },
      },
    })
    
    // Nested write
    const createTwo = prisma.team.create({
      data: {
        name: 'Cool Crew',
        members: {
          create: {
            email: '[email protected]',
          },
        },
      },
    })
    
    // $transaction([]) API
    await prisma.$transaction([createTwo, createOne])

independent writes

bulk operation

  • updateMany
  • deleteMany
  • createMany

추천 사용처

  • 같은 type의 record를 일괄적으로 처리하는 경우

bulk operation 예시

  • user가 이메일을 일괄적으로 읽음처리를 하려는 경우 (one-to-many)

  • 예제 코드

    await prisma.email.updateMany({
      where: {
        user: {
          id: 10,
        },
        unread: true,
      },
      data: {
        unread: false,
      },
    })

주의 사항

  • bulk operation과 nested writes는 동시에 사용 불가능
    • 여러 개의 team과 그 team에 소속한 user를 모두 삭제하려는 경우 ⇒ cascading delete
  • bulk operation과 $transaction([]) API는 동시에 사용 가능

$transaction([]) API

  • independent writes에 대한 일반적인 해결책

  • 어떤 operation이 fail하면 모든 transaction을 roll back

  • 서로 관계 없는 record들에 대해 batch update 하려는 경우 사용

  • $executeRaw(raw query)를 batch하려는 경우

  • 예제 코드

    const id = 9 // User to be deleted
    
    const deletePosts = prisma.post.deleteMany({
      where: {
        userId: id,
      },
    })
    
    const deleteMessages = prisma.privateMessage.deleteMany({
      where: {
        userId: id,
      },
    })
    
    const deleteUser = prisma.user.delete({
      where: {
        id: id,
      },
    })
    
    await prisma.$transaction([deletePosts, deleteMessages, deleteUser]) // Operations succeed or fail together

read, modify, write

[https://en.wikipedia.org/wiki/Read–modify–write](https://en.wikipedia.org/wiki/Read%E2%80%93modify%E2%80%93write)

개념

  • custom logic을 atomic operation처럼 사용할 때 read-modify-write pattern을 적용하는 것
  • value를 읽은 후 value를 조작하고 db에 write

idempotent API를 디자인 하는 방법과 optimistic concurrency control을 사용하는 방법이 있음

idempotent API

  • custom logic을 실행했을 때 결과 값이 언제나 같아야 함
  • user의 email정보를 upsert(update-or-insert)할 때 email column에 unique constraint가 적용되어 있다면 upsert operation은 idempotent

idempotent API 예시

  • slack에서 team에 속한 member들에게 유료 서비스를 제공하려고 함

  • team member의 수를 세고 이 수에 비례하여 결제를 진행한 후 유료 서비스를 제공하는 순서를 거칠 것임; read(member의 수) → create(결제) → modify(유료 서비스 제공) → write(유료 서비스 제공 한다는 것을 db에 기록)

  • 예제 코드

    // Calculate the number of users times the cost per user
    const numTeammates = await prisma.user.count({
      where: {
        teams: {
          some: {
            id: teamId,
          },
        },
      },
    })
    
    // Find customer in Stripe
    let customer = await stripe.customers.get({ externalId: teamID })
    
    if (customer) {
      // If team already exists, update
      customer = await stripe.customers.update({
        externalId: teamId,
        plan: 'plan_id',
        quantity: numTeammates,
      })
    } else {
      customer = await stripe.customers.create({
        // If team does not exist, create customer
        externalId: teamId,
        plan: 'plan_id',
        quantity: numTeammates,
      })
    }
    
    // Update the team with the customer id to indicate that they are a customer
    // and support querying this customer in Stripe from our application code.
    await prisma.team.update({
      data: {
        customerId: customer.id,
      },
      where: {
        id: teamId,
      },
    })

optimistic concurrency control

https://github.com/prisma/prisma/issues/4988

추천 사용처

  • 많은 동시성 요청을 처리하는 경우
  • 동시성 상황이 아주 희박하게 일어날 경우

optimistic concurrency control 예시

  • 영화관의 좌석을 예매하는 경우

  • 예제 코드; im-memory의 값과 db의 값을 비교하여 해결

    const userEmail = '[email protected]'
    const movieName = 'Hidden Figures'
    
    // Find the first available seat
    // availableSeat.version might be 0
    const availableSeat = await client.seat.findFirst({
      where: {
        Movie: {
          name: movieName,
        },
        claimedBy: null,
      },
    })
    
    if (!availableSeat) {
      throw new Error(`Oh no! ${movieName} is all booked.`)
    }
    
    // Only mark the seat as claimed if the availableSeat.version
    // matches the version we're updating. Additionally, increment the
    // version when we perform this update so all other clients trying
    // to book this same seat will have an outdated version.
    const seats = await client.seat.updateMany({
      data: {
        claimedBy: userEmail,
        version: {
          increment: 1,
        },
      },
      where: {
        id: availableSeat.id,
        version: availableSeat.version, // This version field is the key; only claim seat if in-memory version matches database version, indicating that the field has not been updated
      },
    })
    
    if (seats.count === 0) {
      throw new Error(`That seat is already booked! Please try again.`)
    }

interactive transaction

추천 사용처

  • refactoring 하기 어려운 code들이 많을 때 사용

  • 즉 프로젝트를 시작하는 단계가 아니고 마무리 단계에 이르렀을 때 transaction이 필요하다면 사용

  • 예제 코드

    import { PrismaClient } from '@prisma/client'
    const prisma = new PrismaClient()
    
    async function transfer(from: string, to: string, amount: number) {
      return await prisma.$transaction(async (tx) => {
        // 1. Decrement amount from the sender.
        const sender = await tx.account.update({
          data: {
            balance: {
              decrement: amount,
            },
          },
          where: {
            email: from,
          },
        })
    
        // 2. Verify that the sender's balance didn't go below zero.
        if (sender.balance < 0) {
          throw new Error(`${from} doesn't have enough to send ${amount}`)
        }
    
        // 3. Increment the recipient's balance by amount
        const recipient = tx.account.update({
          data: {
            balance: {
              increment: amount,
            },
          },
          where: {
            email: to,
          },
        })
    
        return recipient
      })
    }
    
    async function main() {
      // This transfer is successful
      await transfer('[email protected]', '[email protected]', 100)
      // This transfer fails because Alice doesn't have enough funds in her account
      await transfer('[email protected]', '[email protected]', 100)
    }
    
    main()

주의사항

  • transaction을 오래 열고 있는 것은 db성능저하, dead lock 발생 원인임
  • interactive transaction을 사용할 때 코드에서 network request 혹은 slow query를 실행하는 것을 지양해야함

🌊 ALGOCEAN

TEAM : 강서(대문)구

기획

아키텍처

스프린트 계획회의

데일리스크럼

팀 회고

개발 일지

태호

more

지호

more

지은

more

승규

more

멘토링 일지

Clone this wiki locally