Prisma와 관계
일대다 관계 정의하기
User(1)와 Order(n)사이의 일대다 관계 정의
model User {
id String @id @default(uuid()) ////uuid는 36자로 이루어진 id형식
email String @unique
firstName String
lastName String
address String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
**orders Order[] // 다 모델 배열을 정의한다**
}
model Order {
id String @id @default(uuid())
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
**user User @relation(fields: [userId],references: [id]) //편의성을 위한 관계 필드
userId String //실제 foreign 키 필드**
}
💡관계 필드는 실제 데이터베이스엔 저장되지 않는다
Prisma 클라이언트를 사용할 때 관계 필드를 이용해서 관련된 객체에 접근할 수 있다
일대일, 다대다 관계 정의하기
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid()) ////uuid는 36자로 이루어진 id형식
email String @unique
firstName String
lastName String
address String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orders Order[] //대괄호는 배열, order 여러개를 뜻한다
//1의 부분엔 다 모델의 배열을 정의
userPreference UserPreference? //일대일관계이기 때문에 대괄호를 지우고 물음표를 붙인다
saveProducts Product[] //다대다 관계이기 때문에 타 모델 배열을 저장
}
model Product {
id String @id @default(uuid())
name String
description String?
category Category
price Float
stock Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orderItems OrderItem[]
savedUsers User[] //다대다 관계이기 때문에 타 모델 배열을 저장
}
model UserPreference {
id String @id @default(uuid())
receiveEmail Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields:[userId], references: [id])
userId String @unique // 일대일관계
}
model Order {
id String @id @default(uuid())
status OrderStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id]) //편의성을 위한 관계 필드
//userId 필드가 User 모델의 id 필드를 참조한다는 걸 뜻한다
userId String //실제 foreign 키 필드
//다 부분엔 foreign 키 정의
orderItems OrderItem[]
}
model OrderItem {
id String @id @default(uuid())
unitPrice Float
quantity Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
order Order @relation(fields: [orderId], references: [id])
orderId String
product Product @relation(fields: [productId], references: [id])
productId String
}
enum Category {
FASHION
BEAUTY
SPORTS
ELECTRONICS
HOME_INTERIOR
HOUSEHOLD_SUPPLIES
KITCHENWARE
}
enum OrderStatus {
PENDING
COMPLETE
}
관련된 객체 조회하기
User모델은 UserPreference와 연결되어있다 → User 모델을 조회하면 UserPreferece도 조회하게 설정
관계 필드를 조회하라면 include 프로퍼티를 사용하면 된다!
include:{
userPreference: true, //관계필드 조회
},
UserPreference 객체에서 특정 필드만 조회할 수 있다
select 프로퍼티를 사용하면 된다
include:{
userPreference: {
select:{
receiveEmail: true, //관계필드 조회
},
},
},
유저와 상품 사이에는 찜 관계가 있다
→ 특정 유저가 찜한 상품들을 조회할 수 있는 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);
}));
Computed 필드
다른 필드의 값을 활용해서 계산된 필드를 Computed 필드라고 한다
총합을 나타내는 total 필드
let total = 0;//total 필드
order.orderItems.forEach((orderItem) => {
total += orderItem.unitPrice * orderItem.quantity;
});
order.total = total;
관련된 객체 생성, 수정하기
관련 객체 생성
app.post('/users', asyncHandler(async (req, res) => {
assert(req.body,CreateUser); // 확인하고자하는 데이터 객체와 수퍼스트럭트 객체를 전달하면 된다 (유효성 검사)
// 리퀘스트 바디 내용으로 유저 생성-> create 메소드 사용
const{userPreference, ...userFiedls}=req.body;
const user = await prisma.user.create({
data:{
...userFiedls,
userPreference:{
create:userPreference,//관련된 객체는 create 프로퍼티를 사용해야함
},
},
include:{
userPreference:true,//생성된 데이터를 돌려줄때 userPreference도 같이 돌려준다
},
});
res.status(201).send(user);
}));
관련 객체 수정
app.patch('/users/:id', asyncHandler(async (req, res) => {
assert(req.body,PatchUser);
const { id } = req.params;
// 리퀘스트 바디 내용으로 id에 해당하는 유저 수정 ->update메소드 사용
const user = await prisma.user.update({
where:{ id },
data:{
...userFiedls,
userPreference:{
create:userPreference,//관련된 객체는 create 프로퍼티를 사용해야함
},
},
include:{
userPreference:true,//생성된 데이터를 돌려줄때 userPreference도 같이 돌려준다
},
});
res.send(user);
}));
관련된 객체 연결, 연결 해제하기
일대일, 일대다 관계는 객체를 생성할 때 관련된 객체도 생성한다
다대다 관계는 이미 존재하는 객체관의 관계 생성한다
struct.js
export const PostSavedProduct = s.object({
productId: Uuid,
});
app.js
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: { //연결해제는 disconnect
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
res.send(savedProducts);
})
);
✍️ 요약정리
관계 정의하기
일대다 관계
schema.prisma
model User {
// ...
**orders Order[]**
} 일
model Order {
// ...
**user User @relation(fields: [userId], references: [id])
userId String**
} 다
'다'에 해당하는 모델에는 아래 필드를 정의한다
- 다른 모델을 가리키는 관계 필드(user): @relation 어트리뷰트로 foreign key 필드가 무엇이고, 어떤 필드를 참조하는지 명시한다
- Foreign key 필드(userId)
'일'에 해당하는 모델에는 아래 필드를 정의한다
- 다른 모델 배열을 저장하는 관계 필드(orders)
일대일 관계
schema.prisma
model User {
// ...
**userPreference UserPreference?**
}
model UserPreference {
// ...
**user User @relation(fields: [userId], references: [id])
userId String @unique**
}
Foreign key를 어느 쪽에 정의하든 큰 상관은 없지만, 만약 한쪽 모델이 다른 쪽 모델에 속해 있다면 속해 있는 모델에 정의하는 것이 좋다
Foreign key를 정의하는 모델에는 아래 필드를 정의한다.
- 다른 모델을 가리키는 관계 필드(user): @relation 어트리뷰트로 foreign key 필드가 무엇이고, 어떤 필드를 참조하는지 명시한다.
- Foreign key 필드(userId): @unique으로 설정해줘야한다.
반대쪽 모델에는 아래 필드를 정의한다.
- 다른 모델을 가리키는 옵셔널 관계 필드 (userPreference)
다대다 관계
schema.prisma
model User {
// ...
savedProducts Product[]
}
model Product {
// ...
savedUsers User[]
}
양쪽 모델에 서로의 배열을 저장하는 관계 필드를 정의하면 된다
최소 카디널리티
최소 카디널리티는 스키마로 완벽히 제어하기 어렵다. 유일하게 설정할 수 있는 부분은 Foreign key와 관계 필드를 옵셔널(?)하게 만드는 것.
model User {
// ...
orders Order[]
}
model Order {
// ...
user User @relation(fields: [userId], references: [id])
userId String
}
model User {
// ...
orders Order[]
}
model Order {
// ...
user User? @relation(fields: [userId], references: [id])
userId String?
}
onDelete 옵션
onDelete 옵션은 연결된 데이터가 삭제됐을 때 기존 데이터를 어떻게 처리할지를 정하는 옵션
schema.prisma
model Order {
// ...
user User @relation(fields: [userId], references: [id], onDelete: ...)
userId String
}
- Cascade: userId가 가리키는 유저가 삭제되면 기존 데이터도 삭제된다
- Restrict: userId를 통해 유저를 참조하는 주문이 하나라도 있다면 유저를 삭제할 수 없다
- SetNull: userId가 가리키는 유저가 삭제되면 userId를 NULL로 설정합니다. user와 userId 모두 옵셔널해야 한다
- SetDefault: userId가 가리키는 유저가 삭제되면 userId를 디폴트 값으로 설정합니다. userId 필드에 @default()를 제공해야 한다
관계 필드와 foreign key가 필수일 경우 Restrict가 기본값이고 옵셔널할 경우 SetNull이 기본값
관계 활용하기
Prisma Client에서는 관계 필드들을 자유롭게 이용할 수 있다
schema.prisma
model User {
// ...
orders Order[]
}
model Order {
// ...
user User @relation(fields: [userId], references: [id])
userId String
}
예를 들어 위와 같은 관계가 있을 때 orders 필드와 user 필드는 실제로 어떤 데이터를 저장하지 않지만 (데이터베이스에서는 userId로 유저의 id만 저장하면 된다) Prisma 코드를 작성할 때 사용할 수 있다.
관련된 객체 조회하기
schema.prisma
model User {
// ...
userPreference UserPreference?
}
model UserPreference {
// ...
user User @relation(fields: [userId], references: [id])
userId String @unique
}
userPreference나 user 같은 필드는 기본적으로 조회 결과에 포함되지 않는다. 이런 필드를 같이 조회하려면 include 프로퍼티를 사용해야한다.
app.js
const id = '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317';
const user = await prisma.user.findUniqueOrThrow({
where: { id },
include: {
userPreference: true,
},
});
console.log(user);
{
id: '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317',
email: 'kimyh@example.com',
firstName: '영희',
lastName: '김',
address: '경기도 고양시 봉명로 789번길 21',
createdAt: 2023-07-16T09:30:00.000Z,
updatedAt: 2023-07-16T09:30:00.000Z,
userPreference: {
id: 'e1c1e5c1-5312-4f7b-a3d6-4cbb2b4f8828',
receiveEmail: false,
createdAt: 2023-07-16T09:30:00.000Z,
updatedAt: 2023-07-16T09:30:00.000Z,
userId: '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317'
}
}
네스팅을 이용해서 관련된 객체의 특정 필드만 조회할 수도 있다.
app.js
const id = '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317';
const user = await prisma.user.findUniqueOrThrow({
where: { id },
include: {
userPreference: {
select: {
receiveEmail: true,
},
},
},
});
console.log(user);
{
id: '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317',
email: 'kimyh@example.com',
firstName: '영희',
lastName: '김',
address: '경기도 고양시 봉명로 789번길 21',
createdAt: 2023-07-16T09:30:00.000Z,
updatedAt: 2023-07-16T09:30:00.000Z,
userPreference: { receiveEmail: false }
}
관계 필드가 배열 형태여도 똑같이 include를 사용할 수 있다.
schema.prisma
model User {
// ...
savedProducts Product[]
}
model Product {
// ...
savedUsers User[]
}
app.js
const id = '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317';
const user = await prisma.user.findUniqueOrThrow({
where: { id },
include: {
savedProducts: true,
},
});
res.send(user);
{
id: '6c3a18b0-11c5-4d97-9019-9ebe3c4d1317',
email: 'kimyh@example.com',
firstName: '영희',
lastName: '김',
address: '경기도 고양시 봉명로 789번길 21',
createdAt: 2023-07-16T09:30:00.000Z,
updatedAt: 2023-07-16T09:30:00.000Z,
savedProducts: [
{
id: 'f8013040-b076-4dc4-8677-11be9a17162f',
name: '랑방 샤워젤 세트',
description: '랑방의 향기로운 샤워젤 세트입니다. 피부를 부드럽게 케어하며, 향기로운 샤워 시간을 선사합니다.',
category: 'BEAUTY',
price: 38000,
stock: 20,
createdAt: 2023-07-14T10:00:00.000Z,
updatedAt: 2023-07-14T10:00:00.000Z
},
{
id: '7f70481b-784d-4b0e-bc3e-f05eefc17951',
name: 'Apple AirPods 프로',
description: 'Apple의 AirPods 프로는 탁월한 사운드 품질과 노이즈 캔슬링 기능을 갖춘 무선 이어폰입니다. 간편한 터치 컨트롤과 긴 배터리 수명을 제공합니다.',
category: 'ELECTRONICS',
price: 320000,
stock: 10,
createdAt: 2023-07-14T11:00:00.000Z,
updatedAt: 2023-07-14T11:00:00.000Z
},
{
id: '4e0d9424-3a16-4a5e-9725-0e9d2f9722b3',
name: '베르사체 화장품 세트',
description: '베르사체의 화장품 세트로 화려하고 특별한 분위기를 연출할 수 있습니다. 다양한 아이템으로 구성되어 있으며, 고품질 성분을 사용하여 피부에 부드럽고 안정적인 관리를 제공합니다.',
category: 'BEAUTY',
price: 65000,
stock: 8,
createdAt: 2023-07-14T11:30:00.000Z,
updatedAt: 2023-07-14T11:30:00.000Z
},
{
id: 'a4ff201c-48f7-4963-b317-2e9e4e3e43b7',
name: '랑방 매트 틴트',
description: '랑방 매트 틴트는 풍부한 컬러와 지속력을 제공하는 제품입니다. 입술에 부드럽게 발리며 오래 지속되는 매트한 마무리를 선사합니다.',
category: 'BEAUTY',
price: 35000,
stock: 20,
createdAt: 2023-07-14T16:00:00.000Z,
updatedAt: 2023-07-14T16:00:00.000Z
}
]
}
관련된 객체 생성, 수정하기
객체를 생성하거나 수정할 때 관련된 객체를 동시에 생성하거나 수정할 수 있다.
schema.prisma
model User {
// ...
userPreference UserPreference?
}
model UserPreference {
// ...
user User @relation(fields: [userId], references: [id])
userId String @unique
}
데이터를 data 프로퍼티로 바로 전달하지 않고 관련된 객체 필드에 create 또는 update 프로퍼티를 이용해야 한다.
app.js
/* create */
const postBody = {
email: 'yjkim@example.com',
firstName: '유진',
lastName: '김',
address: '충청북도 청주시 북문로 210번길 5',
userPreference: {
receiveEmail: false,
},
};
const { userPreference, ...userFields } = postBody;
const user = await prisma.user.create({
data: {
...userFields,
userPreference: {
create: userPreference,
},
},
include: {
userPreference: true,
},
});
console.log(user);
{
id: 'd2f4a7fe-0831-462f-9b11-baddb0e4aba2',
email: 'yjkim@example.com',
firstName: '유진',
lastName: '김',
address: '충청북도 청주시 북문로 210번길 5',
createdAt: 2023-08-25T05:12:00.740Z,
updatedAt: 2023-08-25T05:12:00.740Z,
userPreference: {
id: '8dffa6b8-bb2e-4c6e-82ef-053d69b4face',
receiveEmail: false,
createdAt: 2023-08-25T05:12:00.740Z,
updatedAt: 2023-08-25T05:12:00.740Z,
userId: 'd2f4a7fe-0831-462f-9b11-baddb0e4aba2'
}
}
/* update */
const id = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';
const patchBody = {
email: 'honggd2@example.com',
userPreference: {
receiveEmail: false,
},
};
const { userPreference, ...userFields } = patchBody;
const user = await prisma.user.update({
where: { id },
data: {
...userFields,
userPreference: {
update: userPreference,
},
},
include: {
userPreference: true,
},
});
console.log(user);
{
id: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e',
email: 'honggd2@example.com',
firstName: '길동',
lastName: '홍',
address: '서울특별시 강남구 무실로 123번길 45-6',
createdAt: 2023-07-16T09:00:00.000Z,
updatedAt: 2023-08-25T05:15:06.106Z,
userPreference: {
id: '936f5ea4-6e6c-4e5e-91a3-78f5644e1f9a',
receiveEmail: false,
createdAt: 2023-07-16T09:00:00.000Z,
updatedAt: 2023-08-25T05:15:06.106Z,
userId: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e'
}
}
관련된 객체 연결, 연결 해제하기
다대다 관계는 보통 두 객체가 이미 존재하고, 그 사이에 관계를 생성하려고 하는 경우가 많다. 이런 경우 connect 프로퍼티를 이용하면 된다.
schema.prisma
model User {
// ...
savedProducts Product[]
}
model Product {
// ...
savedUsers User[]
}
app.js
const userId = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';
const productId = 'c28a2eaf-4d87-4f9f-ae5b-cbcf73e24253';
const user = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
connect: {
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
console.log(user);
{
id: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e',
email: 'honggd2@example.com',
firstName: '길동',
lastName: '홍',
address: '서울특별시 강남구 무실로 123번길 45-6',
createdAt: 2023-07-16T09:00:00.000Z,
updatedAt: 2023-08-25T05:15:06.106Z,
savedProducts: [
{
id: 'c28a2eaf-4d87-4f9f-ae5b-cbcf73e24253',
name: '쿠진앤에이 오믈렛 팬',
description: '쿠진앤에이의 오믈렛 팬은 오믈렛을 쉽고 빠르게 만들 수 있는 전용 팬입니다. 내열성이 뛰어나며 논스틱 처리로 편리한 사용과 청소가 가능합니다.',
category: 'KITCHENWARE',
price: 25000,
stock: 8,
createdAt: 2023-07-15T13:30:00.000Z,
updatedAt: 2023-07-15T13:30:00.000Z
}
]
}
반대로 연결을 해제하고 싶다면 disconnect 프로퍼티를 이용하면 된다.
const userId = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';
const productId =
const user = await prisma.user.update({
where: { id: userId },
data: {
savedProducts: {
disconnect: {
id: productId,
},
},
},
include: {
savedProducts: true,
},
});
console.log(user);
{
id: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e',
email: 'honggd2@example.com',
firstName: '길동',
lastName: '홍',
address: '서울특별시 강남구 무실로 123번길 45-6',
createdAt: 2023-07-16T09:00:00.000Z,
updatedAt: 2023-08-25T05:15:06.106Z,
savedProducts: []
}
'🏃♀️ 대외활동 > Codeit Boost-Node.js' 카테고리의 다른 글
9주차 스터디 Express 핵심 기능 [코드잇 부스트 백엔드 스터디 ] (0) | 2024.07.31 |
---|---|
8주차 스터디 Git 명령어 및 개념 정리 [코드잇 부스트 백엔드 스터디] (0) | 2024.07.16 |
6주차 스터디 관계형 데이터베이스를 활용한 자바스크립트 서버 만들기(1) [코드잇 부스트 백엔드 스터디] (1) | 2024.07.03 |
6주차 스터디 자바스크립트 백엔드 개발 (2) [코드잇 부스트 백엔드 스터디] (1) | 2024.06.30 |
6주차 스터디 자바스크립트 백엔드 개발 (1) [코드잇 부스트 백엔드 스터디] (0) | 2024.06.28 |