[코드잇] 관계형 데이터베이스를 활용한 자바스크립트 서버 만들기
◆ Prisma 기본기
◆ 관계형 데이터베이스 기본기
◆ Prisma와 관계
◆ 배포하기
◆ Prisma 기본기
Prisma 초기화
백엔드에서 가장 먼저 해야할 일은 필요한 테이블을 Prisma로 정의하는 것임.
npx : node package executor
node package를 실행해주는 툴임.
Prisma가 postgresql를 사용하도록 데이터베이스를 초기화하는 명령어
Prisma 커맨드는 항상 'npx로 시작함.'
npx prisma init --datasource-provider postgresql
윈도우 : username을 postgres로 입력, 비밀번호 입력
맥 : 로그인된 username과 비밀번호 입력하기
Prisma Extension 설치하면 포맷팅도 알아서 해줌.
Alt + shift + F : 포맷팅
User 모델 만들기
모델은 적어도 하나의 unique 필드가 필요하다고 함.
@unique나 @id를 사용하라고 함.
필드들은 기본적으로 필수값이고, 옵셔널하게 만들려면 물음표 추가하면 됨.
address String? 이라고 쓰면 주소는 비워놔도 되는 것임.
model User {
id String @id // 고유 id
email String
firstName String
lastName String
address String? // NULL Nullable 컬럼
}
- id : 필드 이름
- String : 필드 타입
- @id : 어트리뷰트(attribute)
uuid
- 36자로 이루어진 고유 id 형식
- 유저 아이디가 1, 2, 3 이런식으로 이어지면 다음 아이디를 쉽게 추측할 수 있어서 보안성에 문제가 있음. 이를 해결하기 위해 사용함.
model User {
id String @id @default(uuid()) // 고유 id
email String @unique
firstName String
lastName String
address String? // NULL Nullable 컬럼
}
id 필드 정의하는 방법
id String @id @default(uuid())
id Int @id @default(autoincrement())
Prisma Schema 추가 기능
enum(enumerated type)
- 필드의 값이 몇 가지 정해진 값 중 하나일 때 사용함.
- SQLite에서는 enum을 사용할 수 없음.
model User {
// ...
membership Membership @default(BASIC)
}
enum Membership {
BASIC
PREMIUM
}
@@unique
- 여러 필드의 조합이 unique해야 하는 경우 @@unique 어트리뷰트를 사용
- 특정 필드에 종속된 어트리뷰트가 아니기 때문에 모델 아래 부분에 씀.
model User {
id String @id @default(uuid())
email String @unique
firstName String
lastName String
address String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([firstName, lastName])
}
마이그레이션
모델코드를 데이터베이스에 반영하는 과정을 Migration이라고 함.
스키마 파일에 변경사항이 있으면 항상 Migration을 함.
npx prisma migrate dev
가장 처음 마이그레이션은 보통 init이라고 부름.
입력하면 테이블을 생성하는 SQL문이 있음.
모델이 테이블이 되고, 필드가 컬럼이 됨.
스키마 파일에 변화를 주면 마이그레이션을 해줘야 DB에 반영이 됨.
npx prisma studio
Add record를 하면 데이터를 추가할 수 있음.
Save Change를 눌러서 저장함.
테이블에 데이터가 있을 때 마이그레이션하기
- 에러문 의미 : 기존에는 age 필드가 없다가 새로 추가하게 되면, 그 이전에 생성된 데이터들은 age 필드가 비어있게 되므로 이런 오류가 발생함.
- 해결방법 : 새로 추가하고자 하는 필드를 옵셔널 필드로 만들고, 필드값을 채워넣은 후에 필수 필드로 만들면 됨.
프리즈마는 마지막 migration이후 스키마에 있었던 변경사항을 migration으로 만들고 실행함.
migration 파일을 통해 데이터베이스 구조가 변화하는 과정을 알 수 있음.
age에 값을 넣어줌.
@default 어트리뷰트를 사용해서 값이 없는 row에 디폴트값을 넣는 방법도 있음.
age 필드를 다시 지우려고 하면 어떻게 됨?
→ 필드를 지우는 것은 컬럼 자체를 없애는 것이기 때문에 문제가 안 됨.
age 컬럼에 데이터가 있는데 확실히 지울 것인지 물어봄.
Y 누르고 migration 이름 누르면 지워짐.
Prisma Client와 데이터베이스 CRUD
Prisma CRUD를 할 때는 Prisma Client를 통해서 함.
/ 유저 목록 조회
app.get("/users", async (req, res) => {
const users = await prisma.user.findMany();
res.send(users);
});
// id에 해당하는 유저 조회
app.get("/users/:id", async (req, res) => {
const { id } = req.params;
const user = await prisma.user.findUnique({ where: { id } });
res.send(user);
});
// 리퀘스트 바디 내용으로 유저 생성
app.post("/users", async (req, res) => {
const user = await prisma.user.create({ data: req.body });
res.status(201).send(user);
});
// 리퀘스트 바디 내용으로 id에 해당하는 유저 수정
app.patch("/users/:id", async (req, res) => {
const { id } = req.params;
const user = await prisma.user.update({ where: { id }, data: req.body });
res.send(user);
});
// id에 해당하는 유저 삭제
app.delete("/users/:id", async (req, res) => {
const { id } = req.params;
await prisma.user.delete({ where: { id } });
res.sendStatus(204);
데이터베이스 시딩
seed.js
import { PrismaClient } from "@prisma/client";
import { USERS } from "./mock.js";
const prisma = new PrismaClient();
async function main() {
// 기존 데이터 삭제
await prisma.user.deleteMany();
// 목 데이터 삽입
await prisma.user.createMany({
data: USERS,
skipDuplicates: true,
});
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
쿼리 파라미터 처리하기
// 유저 목록 조회
app.get("/users", async (req, res) => {
const { offset = 0, limit = 10, order = "newest" } = req.query;
let orderBy;
switch (order) {
case "oldest":
orderBy = { createdAt: "asc" };
break;
case "newest":
default:
orderBy = { createdAt: "desc" };
}
const users = await prisma.user.findMany({
orderBy,
skip: parseInt(offset),
take: parseInt(limit),
});
res.send(users);
});
Prisma Client 추가 기능
Prisma 메소드
https://www.prisma.io/docs/orm/reference/prisma-client-reference#filter-conditions-and-operators
.findUnique() vs .findFirst()
- .findUnique() : id 필드나 unique한 필드로만 필터할 수 있음.
- .findFirst() : unique하지 않은 필드로 필터해서 객체 하나 조회
.upsert()
- where 조건을 만족하는 객체가 있다면 객체를 업데이트하고, 없다면 생성(insert)함.
- Update + insert
.count()
- 객체의 개수만 필요한 경우 사용하는 메소드
- .findMany()의 배열 길이를 사용하는 것보다 효율적임.
필터 조건
where 프로퍼티에 이름과 값 넣기 : 일치(equal) 연산자 활용됨.
- AND : where 안에 여러 프로퍼티 작성
// category가 'FASHION'이면서 name에 '나이키'가 들어가는 Product들 필터
const products = await prisma.product.findMany({
where: {
category: 'FASHION',
name: {
contains: '나이키',
},
},
});
- OR : OR 연산자 + 대괄호 넣기
// name에 '아디다스'가 들어가거나 '나이키'가 들어가거는 Product들 필터
const products = await prisma.product.findMany({
where: {
OR: [
{
name: {
contains: '아디다스',
},
},
{
name: {
contains: '나이키',
},
},
],
},
});
- NOT(필터 조건을 만족하면 안 되는 경우): NOT 연산자
// name에 '삼성'이 들어가지만 'TV'는 들어가지 않는 Product들 필터
const products = await prisma.product.findMany({
where: {
name: {
contains: '삼성',
},
NOT: {
name: {
contains: 'TV',
},
},
},
});
유효성 검사
- request로 전달받는 데이터는 항상 유효성 검사를 해야됨.
ex. 데이터에 필요한 필드가 있는지, 모델에 정의되지 않은 필드는 없는지, 각 필드가 올바른 형식인지 등 - 'superstruct'라는 라이브러리를 활용하여 유효성 검사를 할 것임.
예상하는 데이터 형식을 정의하고, 실제 받은 데이터가 그 형식과 일치하는지 확인하는 것
• define 함수 : 새로운 타입 만들 수 있음.
- Email 타입은 isEmail 함수를 통과하는 값들임.
• size 함수 : string 타입의 길이를 제한함.
import * as s from "superstruct";
import isEmail from "is-email";
export const CreateUser = s.object({
email: s.define('Email', isEmail),
firstName: s.size(s.string(), 1, 30),
lastName: s.size(s.string(), 1, 30),
address: s.string(),
});
https://docs.superstructjs.org/api-reference/types
Types | Superstruct
Superstruct exposes factory functions for a variety of common JavaScript (and TypeScript) types. You can also define your own custom validation functions using the struct factory. any structs accept any value as valid. 🤖 Note that if you're using TypeSc
docs.superstructjs.org
이거 참고해서 작성하면 됨.
price: s.min(s.number(), 0),
오류 처리하기
asyncHandler 함수는 Express 라우트 핸들러를 wrapping하여, 비동기 핸들러에서 발생하는 오류를 통합적으로 처리함.
function asyncHandler(handler) {
return async function (req, res) {
try {
await handler(req, res);
} catch (e) {
// 유효성 검사 오류
if (
e.name === "StructError" || // 유효성 검사 라이브러리에서 발생하는 오류 (ex. superstruct)
e instanceof Prisma.PrismaClientValidationError // 프리즈마의 유효성 검사 오류
) {
res.status(400).send({ message: e.message });
// 특정 요청에 대해 리소스를 찾지 못한 경우
} else if (
e instanceof Prisma.PrismaClientKnownRequestError &&
e.code === "P2025" // 프리즈마의 KnownRequestError로, 특정 리소스를 찾지 못했음을 나타냄
) {
res.sendStatus(404);
// 기타 서버 오류인 경우
} else {
res.status(500).send({ message: e.message });
}
}
};
}
findUnique 메소드에 없는 id값을 넣으면 null을 반환함.
→ findUniqueOrThrow 메소드를 사용하면 됨.
// id에 해당하는 유저 조회
app.get(
"/users/:id",
asyncHandler(async (req, res) => {
const { id } = req.params;
const user = await prisma.user.findUniqueOrThrow({ where: { id } });
res.send(user);
})
);
◆ 관계형 데이터베이스 기본기
Primary Key와 Foreign Key
- Primary Key : 사용자가 정하는 컬럼
ORM을 사용하면 자동으로 Primary Key를 정해주는 경우가 많음. - Foreign Key : 다른 테이블의 Primary Key를 참조하는 컬럼
데이터 모델링과 ER 모델
데이터 모델링
서비스에 정확히 어떤 데이터가 필요하고 데이터 간 어떤 관계가 있는지 파악해서 정교하게 표현하는 것
ER 모델 (Entity-Relationship 모델)
ER 다이어그램
개체(Entity), 속성(Attribute), 관계(Relationship)
개체 (Entity) | 현실 세계의 사물 또는 객체 하나의 개체 = 하나의 테이블 |
ex. 유저, 상품, 주문 |
속성 (Attribute) | 개체의 세부 정보 하나의 속성 = 하나의 컬럼 |
ex. 유저의 성, 이름, 이메일 |
관계 (Relationship) | 개체 간의 관계 | ex. 유저는 주문을 할 수 있다, 주문은 여러 상품을 포함한다 |
ER 모델링 : 개체, 속성, 관계 후보 찾기
- Business Rule (사업 규칙) : 사업이나 서비스가 운영되기 위해 따르는 규칙
- 하나의 값으로 표현할 수 없는 명사 : 개체의 후보
하나의 값으로 표현할 수 있는 명사 : 속성의 후보
동사 : 관계의 후보
quantity(주문 수량)과 unitPrice(주문 시 상품 가격)은 주문, 상품 중 어느 한 곳에 넣기 애매함.
→ 어떤 "상품" 1개가 "주문"에 포함되어 있을 때 이 관계에 대한 추가정보임.
관계에 대한 속성이 있을 때는 관계 사이에 추가 개체를 만들고, 그곳에 정보를 저장해야함.
ER 모델링 : 카디널리티
카디널리티(cardinality)
- 모델링에 있어서 중요한 부분
- 개체 간의 관계가 있을 때, A개체 1개가 B 개체와 몇 개가 연결될 수 있고, B 개체 1개가 A 개체 몇 개와 연결될 수 있는지를 뜻함.
- 일대다 관계 : 하나의 주문은 1명의 사용자가 함.
Crow's Foot Notation
1은 짧은 수직선(-), 다수는 줄 세 개(三)로 나타냄.
지금까지 정한 카디널리티는 어떤 개체가 다른 개체에 최대 몇 개까지 연결될 수 있는지 나타냄.
ex. 유저는 주문을 여러 개 할 수 있지만, 하나도 안 할수도 있음.
카디널리티를 표시할 때에는 최소 카디널리티도 표시하는게 좋음.
ex. 유저는 최소 0개의 주문을 할 수 있지만, 주문은 유저가 생성하기 때문에 꼭 유저가 1명이 있음.
0은 동그라미(○), 1은 선 1개(-)로, 최대 카디널리티 바깥쪽에 표시함.
최소 카디널리티는 0과 1이 될 수 있고, 최대 카디널리티는 1과 다가 될 수 있음.
ER 모델에서 데이터베이스 테이블로
개체 간의 관계는 Foreign Key를 이용해서 배울 수 있음.
Foreign Key는 타 테이블의 Primary Key를 참조하는 컬럼.
일대일(1:1)
- 다쪽 개체의 테이블에 Foreign Key를 추가
일대다(1:N)
- 어느 한쪽 테이블에 Foreign Key를 추가하고 Unique 속성을 걸면 됨.
- 다른 개체에 속해 있는 쪽(ex. UserPreference)에 Foreign Key를 정의함.
다대다(M:N)
- 테이블에 직접 Foreign Key를 정의하지 않음.
- 하나의 새로운 Junction 테이블이라는 것을 만들면 됨.
(foreign key 1개만으로는 junction 테이블을 저장할 수 없기 때문임.) - Prisma 같은 ORM을 사용하면 junction table을 알아서 만들어주는 경우가 많음.
일대다(1:N)
- 최소 카디널리티 : foreign key 컬럼의 null값 허용 여부
- foreign key 컬럼에 null값을 허용하면 주문에 userId가 없을 수 있음.
- 주문에 연결되어 있는 user가 없는 것임.
→ user쪽의 최소 카디널리티가 0이 됨. - 반면 foreign key 컬럼에 null 값을 허용하지 않는다면 모든 주문은 userId가 있음.
→ user쪽의 최소 카디널리티가 1이 됨. - 반대로 한 유저가 최소 몇 개의 주문과 연결될 수 있는지 생각해보면 0임.
userId를 가리키는 주문이 하나도 없을 수 있기 때문임.
→ foreign key 쪽의 최소 카디널리티를 1로 만들려면 유저마다 유저를 가리키는 주문이 적어도 1개는 있어야 됨.
이건 컬럼의 제약조건으로 구현하기 힘듦. 코드로 확인하는 방법으로 해야됨.
일대일(1:1)
- foreign key가 가리키고 있는 반대쪽의 최소 카디널리티는 foreign key null값 여부를 통해 0, 1로 만들 수 있음.
- foreign key쪽은 데이터베이스 레벨에서는 제어하기 어렵고, 기본적으로 0이 됨.
- user마다 user을 가리키는 UserPreference가 꼭 있게 하는 것은 어려움.
다대다(N:N)
- junction table(연결 테이블)을 사용함.
- 연결 테이블이 아예 비어있을 수도 있기 때문에 양쪽의 최소 카디널리티는 0임.
- 다대다 관계가 발생하는 실제 상황을 생각해보면 최소 카디널리티가 둘 다 0인 경우가 많기 때문에 이 부분에 대해서 신경쓰지 않아도 됨.
◆ Prisma와 관계
일대다 관계 정의하기
새로운 모델 생성 후 마이그레이션 해주기
User와 Order 사이의 일대다 관계를 정의
- 한 명의 유저는 여러 주문을 생성할 수 있고, 하나의 주문은 한 유저의 것임.
- Order 모델의 userId를 저장하는 foreign key 필드를 추가하면 됨.
user User 만 입력하고 저장하니 자동으로 포맷팅 됨.
여기에서 userId 필드가 실제 foreign key 필드이고, 나머지 User와 Order 필드는 편의성을 위한 관계 필드임.
관계 필드
- 데이터베이스에는 실제로 저장되지 않고, Prisma Client를 사용할 때 관계 필드를 이용해서 관련된 개체에 접근할 수 있음.
- ex. order 객체가 있으면 order.user 프로퍼티로 유저에 접근할 수 있음.
userId 필드가 user의 id 필드를 참조한다는 뜻
userId가 foreign key라는 것임.
user User @relation(fields: [userId], references: [id])
user 모델의 id가 integer 타입이었다면 userId 타입도 integer가 됨.
프리즈마 스튜디오에서 확인
실제로 데이터베이스에 저장되는 것은 foreign key userId임.
요약
일대다 관계를 정의할 때
'다'에 해당하는 모델 | '일'에 해당하는 모델 |
![]() |
![]() |
'일'의 모델을 가리키는 필드 '일'의 모델 id를 가리키는 필드를 저장 |
'다' 모델 배열을 정의 |
일대일, 다대다 관계 정의하기
일대일 관계 정의하기
User모델과 UserPreference 모델
- foreign key 필드는 unique(@unique)해야 함.
- foreign key가 참조하는 모델의 관계 필드는 옵셔널(UserPreference?)
→ foreign key가 있는 UserPreference 쪽 카디널리티를 최소 0으로 설정해준 것임.
다대다 관계 정의하기
User와 Product 모델
- Prisma에서는 연결 테이블을 직접 구현할 필요가 없음.
- 각 모델에 타 모델의 배열을 저장하는 필드를 추가하면 됨.
이 상태로 저장하면 Prisma가 알아서 연결 테이블을 만들어줌.
Relation의 onDelete
관계를 정의할 때 @relation이라는 attribute를 사용했음.
relation 어트리뷰트 중 ondelete라는 옵션이 있음.
- onDelete : 연결된 데이터가 삭제됐을 때 기존 데이터를 어떻게 처리할 것인지
onDelete에 사용하는 옵션 4개
- Cascade : Foreign Key가 가리키는 데이터가 삭제되면 기존 데이터도 삭제됨.
- Restrict : 특정 데이터를 참조하는 데이터들이 있으면 데이터를 삭제하지 못함.
- SetNull : Foreign Key가 가리키는 데이터가 삭제되면 Foreign Key를 NULL로 설정
- SetDefault : Foreign Key가 가리키는 데이터가 삭제되면 Foreign Key를 디폴트 값으로 설정
무조건 큰 따옴표를 사용해야함.
model Order {
// ...
user User @relation(fields: [userId], references: [id], onDelete: SetDefault)
userId String @default("Anonymous")
}
model Order {
// ...
user User @relation(fields: [userId], references: [id], onDelete: SetDefault)
userId String @default("Anonymous")
}
relation 필드가
- 필수라면 onDelete는 Restrict가 기본값이고,
- 옵셔널하다면 onDelete는 SetNull이 기본값임.
각 관계에서 어떤 옵션이 적합한지 생각해보면서 onDelete를 설정해보기
- User와 User Preference : user가 삭제되는 경우 user preference가 필요없음 → onDelete: Cascade
- User와 Order : Order가 삭제되어도 주문에 대한 기록은 남겨두면 좋을듯함. → onDelete: SetNull
관련된 객체 조회하기
현재 http 요청을 보내면 user에 대한 정보만 나옴.
@include를 이용해서 관계 필드를 조회할 수 있음.
app.get(
"/users",
asyncHandler(async (req, res) => {
const { offset = 0, limit = 10, order = "newest" } = req.query;
let orderBy;
switch (order) {
case "oldest":
orderBy = { createdAt: "asc" };
break;
case "newest":
default:
orderBy = { createdAt: "desc" };
}
const users = await prisma.user.findMany({
orderBy,
skip: parseInt(offset),
take: parseInt(limit),
include: {
// include 안에서는 관계필드만 suggest 함.
userPreference: true,
},
});
res.send(users);
})
);
GET 요청을 보내니 userPreference까지 조회됨.
@select 프로퍼티를 사용하여 userPreference 전체가 아닌, userPreference의 특정 필드만 조회할 수 있음.
app.get(
"/users",
asyncHandler(async (req, res) => {
const { offset = 0, limit = 10, order = "newest" } = req.query;
let orderBy;
switch (order) {
case "oldest":
orderBy = { createdAt: "asc" };
break;
case "newest":
default:
orderBy = { createdAt: "desc" };
}
const users = await prisma.user.findMany({
orderBy,
skip: parseInt(offset),
take: parseInt(limit),
include: {
userPreference: {
select: { // receiveEmail만 true로 체크
receiveEmail: true,
},
},
},
select: { email: true },
});
res.send(users);
})
);
특정 유저가 찜한 상품들의 목록을 볼 수 있는 API
app.get(
"/users/:id/saved-products",
asyncHandler(async (req, res) => {
const { id } = req.params;
const { savedProducts } = await prisma.user.findUniqueOrThrow({
where: { id },
include: {
savedProducts: true,
},
});
res.send(savedProducts);
})
API를 테스트하면 유저의 찜 목록이 잘 돌아옴.
Computed 필드
computed 필드 : 다른 필드의 값을 활용해서 계산된 필드
프리즈마는 결과를 자바스크립트 객체형태로 돌려주기 때문에 결과에 연산을 하기 편함.
app.get(
"/orders/:id",
asyncHandler(async (req, res) => {
const { id } = req.params;
const order = await prisma.order.findUniqueOrThrow({
where: { id },
include: {
orderItems: true,
},
});
const total = order.orderItems.reduce(
(acc, item) => acc + item.unitPrice * item.quantity,
0
);
order.total = total;
res.send({ order });
})
);
관련된 객체 생성, 수정하기
관련된 객체 생성하기
관련된 객체는 데이터를 그냥 넘겨주면 안 되고, create 프로퍼티를 사용해야함.
// 리퀘스트 바디 내용으로 유저 생성
app.post(
"/users",
asyncHandler(async (req, res) => {
assert(req.body, CreateUser);
const { userPreference, ...userFields } = req.body;
const user = await prisma.user.create({
data: {
...userFields,
userPreference: {
create: userPreference,
},
},
include: {
userPreference: true,
},
});
res.status(201).send(user);
})
);
관련된 객체 수정하기
// 리퀘스트 바디 내용으로 id에 해당하는 유저 수정
app.patch(
"/users/:id",
asyncHandler(async (req, res) => {
assert(req.body, PatchUser);
const { id } = req.params;
const { userPreference, ...userFields } = req.body;
const user = await prisma.user.update({
where: { id },
data: {
...userFields,
userPreference: {
update: userPreference,
},
},
include: {
userPreference: true,
},
});
res.send(user);
})
);
관련된 객체 연결, 연결 해제하기
다대다 관계는 보통 두 객체가 이미 존재하고, 그 사이에 관계를 생성하려고 하는 경우가 많음.
유저가 상품을 찜하는 경우, 유저와 상품은 이미 DB에 존재함.
유저가 상품을 찜할 때는 두 개체간의 관계만 생성해주면 됨.
존재하는 개체간의 관계를 생성하려면 connect 문법을 사용하면 됨.
app.post(
"/users/:id/saved-products",
asyncHandler(async (req, res) => {
assert(req.body, PostSavedProduct);
const { id: userId } = req.params;
const { productId } = req.body;
const { savedProducts } = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
connect: {
// 연결테이블에 유저와 상품ID값을 저장해서 연결해줌.
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
res.send(savedProducts);
})
);
connect/disconnect로 연결 설정 가능함.
비즈니스 로직: 주문 상품 재고 확인하기
주문을 생성하면 상품 재고도 변경해줘야함.
pp.post(
"/orders",
asyncHandler(async (req, res) => {
assert(req.body, CreateOrder);
const { userId, orderItems } = req.body;
const productIds = orderItems.map((orderItem) => orderItem.productId);
const products = await prisma.product.findMany({
where: { id: { in: productIds } },
});
function getQuantity(productId) {
const orderItem = orderItems.find(
(orderItem) => orderItem.productId === productId
);
return orderItem.quantity;
}
// 재고 확인
const isSufficientStock = products.every((product) => {
const { id, stock } = product;
return stock >= getQuantity(id);
});
if (!isSufficientStock) {
throw new Error("Insufficient Stock");
}
const order = await prisma.order.create({
data: {
userId,
orderItems: {
create: orderItems,
},
},
include: {
orderItems: true,
},
});
res.status(201).send(order);
})
);
$transaction으로 쿼리 안전하게 수행하기
일부 쿼리만 실행되는 것을 방지하기 위해서 관계형 데이터베이스들은 트랜잭션을 지원함.
트랜잭션은 여러 쿼리, 즉 데이터베이스 작업을 묶어서 실행하는 것.
트랜잭션을 사용하면 모든 작업이 성공하거나 실패한다.
프리즈마에서는 $transaction 메소드 사용
// 주문 생성
const order = await prisma.order.create({
data: {
userId,
orderItems: {
create: orderItems,
},
},
include: {
orderItems: true,
},
});
// 재고 감소
const queries = productIds.map((productId) =>
prisma.product.update({
where: { id: 123 },
data: { stock: { decrement: getQuantity(productId) } },
})
);
주문 생성을 생성한 후, 재고를 감소함.
만약 예상치 못하게 서버가 다운되거나 오류가 난다면, 주문 생성만 될 수도 있음.
app.post(
"/orders",
asyncHandler(async (req, res) => {
assert(req.body, CreateOrder);
const { userId, orderItems } = req.body;
const productIds = orderItems.map((orderItem) => orderItem.productId);
const products = await prisma.product.findMany({
where: { id: { in: productIds } },
});
function getQuantity(productId) {
const orderItem = orderItems.find(
(orderItem) => orderItem.productId === productId
);
return orderItem.quantity;
}
// 재고 확인
const isSufficientStock = products.every((product) => {
const { id, stock } = product;
return stock >= getQuantity(id);
});
if (!isSufficientStock) {
throw new Error("Insufficient Stock");
}
// order 생성하고 감소
const queries = productIds.map((productId) =>
prisma.product.update({
where: { id: productId },
data: { stock: { decrement: getQuantity(productId) } },
})
);
const [order] = await prisma.$transaction([
prisma.order.create({
data: {
userId,
orderItems: {
create: orderItems,
},
},
include: {
orderItems: true,
},
}),
...queries,
]);
await prisma.$transaction();
await Promise.all(queries);
res.status(201).send(order);
})
);