[태호] prisma transaction 공식문서 정리 - boostcampwm2023/web04-ALGOCEAN GitHub Wiki
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
추천 사용처
- 많은 동시성 요청을 처리하는 경우
- 동시성 상황이 아주 희박하게 일어날 경우
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를 실행하는 것을 지양해야함