Super Kawaii Cute Cat Kaoani
본문 바로가기
🏃‍♀️ 대외활동/Codeit Boost-Node.js

6주차 스터디 관계형 데이터베이스를 활용한 자바스크립트 서버 만들기(2) [코드잇 부스트 백엔드 스터디]

by wonee1 2024. 7. 3.
728x90

 

 

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: []
}
728x90