책 정보를 수정하는 API를 작성하던 중 문제를 마주했다. 책 정보를 update하기 위해서 해당하는 책 정보와 그 책과 관계를 맺는 Quotes 정보를 함께 업데이트할 필요가 있었다. 단일 책 정보만을 업데이트 하는 건 간단하지만 매핑 되어 있는 인용문을 함께 업데이트 하려다 보니 난관에 부딪혔다.
천천히 문제를 해결해보자.
1. 문제 이해
const updatedBook = await prisma.book.update({
where: {
id: id,
},
data: {
genreId: genreId,
title: title,
description: description,
publisher: publisher,
author: authors,
thumbnailUrl: thumbnailUrl,
quotes: quotes,
},
});
기존 작성 코드. quotes는 단순 배열 데이터가 아닌 다른 테이블로 연결되어 있기 때문에 update는 정상적으로 동작하지 않는다.
책과 인용문을 일대다로 연결되어 있다.
quotes를 제외한 책 정보의 업데이트가 가능하다.
기존에 존재하던 인용문 수정이 가능하다.
기존에 존재하던 이용문 삭제가 가능하다.
새로운 인용문의 추가가 가능하다.
2. 해결 과정
책 테이블을 업데이트 하는 것과 인용문 테이블을 업데이트 하는 것을 별도로 진행해줘야 할 것 같다.
const updatedBook = prisma.book.update({
where: { id: id },
data: {
genreId: genreId,
title: title,
description: description,
publisher: publisher,
author: authors,
thumbnailUrl: thumbnailUrl,
},
});
const quoteUpserts = quotes.map((quote: Quote) =>
prisma.quote.upsert({
where: { id: quote.id },
update: { quote: quote.quote, page: quote.page },
create: { quote: quote.quote, page: quote.page, BookId: id },
})
);
데이터베이스에서 책(book)을 업데이트하고, 책에 연결된 인용구(quote)들을 추가 또는 업데이트하는 작업을 수행하는 코드를 만들었다. 이 코드는 트랜잭션을 사용하고 있지 않는다. 즉, 여러 데이터베이스 변경 작업이 한 번에 이루어지는 것을 보장하지 않기에 일부 작업이 실패하면 일관성 있는 데이터베이스 상태를 유지하기 어려워진다.
prisma에서는 $transaction메서드를 제공해준다. 사용해보자.
$transaction: 여러 쿼리를 하나의 데이터베이스 트랜잭션으로 그룹화할 수 있는 Prisma 메소드이다. 트랜잭션은 일련의 데이터베이스 연산을 하나의 논리적 작업 단위로 묶는 것으로, 이 작업 단위 전체가 성공적으로 완료되거나 (모든 연산이 성공하거나) 전혀 완료되지 않아야 (모든 연산이 실패하거나 롤백되어야)한다. 트랜잭션은 데이터의 일관성을 유지하고, 여러 연산을 한 번에 수행할 때 발생할 수 있는 문제를 방지하는 데 중요하다.
const transaction = await prisma.$transaction([
updatedBook,
...quoteUpserts,
]);
quoteUpserts를 스프테드 연산자로 처리하는 이유는, quoteUpserts 배열이 가지고 있는 각각의 객체는 Promise 객체이다. 즉, quotes 배열에 대한 처리를 Promise 객체 형태로 새로운 배열에 저장하여 반환한 결과가 quoteUperts이기 때문이다.
하지만 위 quoteUpserts는 정상적으로 동작하지 않았다. 위 코드에서 새로 추가된 quotes의 경우 id를 가지고 있지 않기 때문에, where: {id: quote.id}는 undefined로 찾게 된다. 이렇게 하면 아무런 레코드를 찾지 못하고 create를 수행하겠거니 했는데 원하는대로 동작하지 않았다.
const quoteUpserts = quotes.map((quote: Quote) =>
quote.id
? prisma.quote.update({
where: { id: quote.id },
data: { quote: quote.quote, page: quote.page, BookId: id },
})
: prisma.quote.create({
data: { quote: quote.quote, page: quote.page, BookId: id },
})
);
그래서 quote.id의 존재 유뮤에 따라 기존에 id를 가지고 있던 이용문은 update를 해주고 id가 없는 즉, 새로 추가된 인용문은 create를 수행해줬다.
기존에 존재하던 이용문의 수정과 새로운 이용문의 추가가 가능해졌다!
이제 기존에 존재하던 인용문의 삭제만 완료하면 된다.
const existingQuotes = await prisma.quote.findMany({
where: { BookId: id },
});
const quotesToDelete = existingQuotes.filter(
(eq) => !quotes.some((quote: Quote) => quote.id === eq.id)
);
const quoteDeletes = quotesToDelete.map((quote: Quote) =>
prisma.quote.delete({ where: { id: quote.id } })
);
수정하려는 책의 기존에 있던 인용문들을 전부 불러왔다. 그러고 난 뒤 수정된 quotes배열과 기존에 존재하던 exisingQuotes배열의 filtering을 수행하여 삭제해야할 인용문을 뽑아온다.
그리고 delete를 수행하는 Promise 객체를 반환한다. 이것 또한 transaction에 담아서 병렬로 처리해준다.
const transaction = await prisma.$transaction([
updatedBook,
...quoteDeletes,
...quoteUpserts,
]);
나중엔 이 코드의 성능을 개선해보도록 하자.