CodeIt Fs 2nd/강의내용정리

관계형 DB를 활용한 JS 서버 만들기

localslave 2024. 9. 19. 00:19

기존에 사용하던 mongodb에서 RDB인 postgreSQL로 변경하고, ORM으로 Prisma를 사용한다.

prisma.schema에 스키마를 정의한다.

model User {
  id        String   @id @default(uuid())
  nickname  String
  image     String?  @default("")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

필드명 / 필드타입 / 어트리뷰트 순으로 정의한다.

필드타입 뒤에 ?를 붙이면 Nullable(Optional) 필드가 된다.

@id : id필드로 사용한다. 유니크해야함.

@unique: id필드로 사용하지 않지만 유니크해야함.

@default: 디폴트값을 설정함.

@updatedAt: updatedAt 필드 설정할때 필요한 방식으로 설정해줌.

enum 타입을 지원한다.

enum Membership {
  BASIC
  PREMIUM
}

필드타입으로 사용 가능하다.

@@unique: 둘 이상의 필드 조합이 유니크해야할 경우에 사용한다.

@index, @@index: 인덱스로 사용한다. 검색 등을 할때 기준이 된다.

자세한건 https://www.prisma.io/docs/orm/reference/prisma-schema-reference#model-field-scalar-types를 참조.


스키마가 변경될 경우 마이그레이션을 해줘야한다. 개발환경에서 마이그레이션은 npx prisma migrate dev로 한다. 윈도우 환경에선 존재하지 않는 DB를 참조하려할때 자동으로 생성하지 못하고 에러가 날 수 있다. 이럴땐 수동으로 생성해줘야한다.


특정 테이블에 접근해 작업할 때, 다음과 같이 사용한다.

const users = await prisma.user.findMany();

const user = await prisma.user.findUnique({
    where: { id },
});

const user = await prisma.user.create({
    data: req.body,
});

const user = await prisma.user.update({
    where: { id },
    data: req.body,
});

const user = await prisma.user.delete({
    where: { id },
});

prisma.모델명.메소드명({옵션})

자세한건 https://www.prisma.io/docs/orm/reference/prisma-client-reference를 참조.


시딩할땐 다음과 같다.

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);
  });

대부분 시딩은 package.json에 스크립트를 등록해서 스크립트로 실행한다. npm run쪽 스크립트에 등록해서 npm run seed, 혹은 prisma쪽 스크립트에 등록해서 npx prisma db seed로 사용한다.


강의 내용 정리 페이지

더보기

Prisma 설치와 초기화

Prisma를 사용하려면 prisma 패키지와 @prisma/client 패키지를 설치해야 합니다.

npm install prisma --save-dev
npm install @prisma/client

prisma는 Prisma 관련 커맨드를 실행하는 데 필요한 라이브러리이고 @prisma/client는 Prisma 관련 코드를 실행하기 위해 필요한 라이브러리입니다. prisma는 보통 dev dependency로 설치합니다.

VS Code를 사용한다면 Prisma 익스텐션도 설치해 주는 것이 좋습니다.

설치를 완료했으면 아래 커맨드로 초기화를 할 수 있습니다.

npx prisma init --datasource-provider db-type

db-type은 postgresql, mysql, sqlite 등으로 설정할 수 있습니다. .env 파일에 있는 정보를 환경에 맞게 수정하세요.

postgrsql을 사용하려면 macOS에서는 컴퓨터 유저 이름과 비밀번호, Windows에서는 postgres와 설치 시 설정한 비밀번호를 사용하면 됩니다.

Prisma Schema

schema.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())
  email     String   @unique
  firstName String
  lastName  String
  address   String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model Product {
  id          String   @id @default(uuid())
  name        String
  description String?
  category    Category
  price       Float
  stock       Int
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt
}

enum Category {
  FASHION
  BEAUTY
  SPORTS
  ELECTRONICS
  HOME_INTERIOR
  HOUSEHOLD_SUPPLIES
  KITCHENWARE
}

각 필드는 필드 이름, 필드 타입, 어트리뷰트(옵셔널)로 구성돼 있습니다. 필드 타입은 ?를 사용해서 필수 여부를 정할 수 있고 자주 사용하는 어트리뷰트는 아래와 같습니다.

  • @id: 필드가 id라는 것을 나타냄
  • @unique: 고유 필드(값이 중복될 수 없음)라는 것을 나타냄
  • @default: 디폴트 값을 설정할 때 사용
  • @updatedAt: 객체가 수정될 때마다 수정 시간을 필드에 기록함

위처럼 필드의 값이 정해진 값 중 하나라면 enum을 사용할 수도 있습니다.

Prisma Migration

schema.prisma 파일을 변경하면 마이그레이션이라는 것을 해 줘야 합니다. 마이그레이션은 스키마 파일의 내용을 데이터베이스에 반영해 주는 것입니다. 예를 들어 schema.prisma에 새로운 모델을 정의하고 마이그레이션을 하면 Prisma는 새로운 테이블을 생성하는 SQL문을 만들어서 실행해 줍니다.

npx prisma migrate dev

Prisma Client

Prisma Client는 모델 정보를 저장하고 있고 이걸 기반으로 데이터베이스와 상호작용합니다. 마이그레이션을 할 때마다 최신 모델 정보를 기반으로 Prisma Client를 생성해 주죠. 모든 데이터베이스 연산은 Prisma Client를 통해 이루어집니다.

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

연산은 prisma.model.method() 형태이며 Promise를 리턴합니다. 이 연산을 '쿼리'라고도 부릅니다.

const id = 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e';

const userData = {
  email: 'yjkim@example.com',
  firstName: '유진',
  lastName: '김',
  address: '충청북도 청주시 북문로 210번길 5',
};;

// 여러 유저 조회
const users = await prisma.user.findMany();

// 유저 1명 조회
const user = await prisma.user.findUnique({
  where: {
    id,
  },
});

// 유저 생성
const user = await prisma.user.create({
  data: userData
});

// 유저 수정
const user = await prisma.user.update({
  where: {
    id,
  },
  data: {
    email: 'yjkim2@example.com',
  },
});

// 유저 삭제
const user = await prisma.user.delete({
  where: {
    id,
  },
});

where는 필터를 할 때 사용하고 data는 데이터를 전달할 때 사용합니다.

.findMany() 메소드에는 아래 프로퍼티도 많이 사용됩니다.

  • orderBy: 정렬 기준
  • skip: 건너뛸 데이터 개수
  • take: 조회할 데이터 개수

예를 들어 URL 쿼리 파라미터와 아래와 같이 사용할 수 있습니다.

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 Client를 이용해서 데이터베이스 시딩도 할 수 있습니다. .createMany() 메소드를 사용하면 배열 데이터를 한꺼번에 삽입할 수 있습니다(SQLite에서는 메소드를 사용할 수 없습니다).

seed.js

import { PrismaClient } from '@prisma/client';
import { USERS, PRODUCTS } from './mock.js';

const prisma = new PrismaClient();

async function main() {
  // 기존 데이터 삭제
  await prisma.user.deleteMany();
  await prisma.product.deleteMany();

  // 목 데이터 삽입
  await prisma.user.createMany({
    data: USERS,
    skipDuplicates: true,
  });

  await prisma.product.createMany({
    data: PRODUCTS,
    skipDuplicates: true,
  });
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

seed.js

const USERS = [
  // ...
];

const PRODUCTS = [
  // ...
];

package.json 파일에 seed 커맨드를 정의하면 Prisma 커맨드로 시딩을 할 수 있습니다.

package.json

{
  // ...
  "prisma": {
    "seed": "node prisma/seed.js"
  }
}

prisma/seed.js 부분은 시드 연산을 실행하는 파일 경로입니다.

아래 커맨드로 시딩을 할 수 있습니다.

npx prisma db seed

유효성 검사

다양한 방식으로 유효성 검사를 할 수 있지만 이번 토픽에서는 superstruct라는 라이브러리로 유효성 검사를 진행했습니다. 예상하는 데이터 형식을 정의하고 실제 데이터를 비교하는 방식인데요. 먼저 패키지들을 설치해야 합니다.

npm install superstruct is-email is-uuid

is-email은 이메일 형식을 확인하고 싶은 경우 설치하시면 되고 is-uuid는 UUID 형식을 확인하고 싶은 경우 설치하시면 됩니다.

superstruct 라이브러리의 .string(), .number(), .integer(), .boolean(), .define(), .object(), .enums(), .array(), .partial() 타입들로 틀을 정의하고 .size(), .min(), .max() 함수로 제약을 추가하면 됩니다.

structs.js

import * as s from 'superstruct';

const CATEGORIES = [
  'FASHION',
  'BEAUTY',
  'SPORTS',
  'ELECTRONICS',
  'HOME_INTERIOR',
  'HOUSEHOLD_SUPPLIES',
  'KITCHENWARE',
];

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(),
});

export const PatchUser = s.partial(CreateUser);

export const CreateProduct = s.object({
  name: s.size(s.string(), 1, 60),
  description: s.string(),
  category: s.enums(CATEGORIES),
  price: s.min(s.number(), 0),
  stock: s.min(s.integer(), 0),
});

export const PatchProduct = s.partial(CreateProduct);

데이터를 비교할 때는 assert() 함수를 사용하면 됩니다.

app.js

import { assert } from 'superstruct';
import { CreateUser } from './structs.js';

// ...

app.post('/users', async (req, res) => {
  assert(req.body, CreateUser); // CreateUser 형식이 아니라면 오류 발생
  // ...
});

오류 처리

이번 토픽에서는 아래와 같이 오류를 처리했습니다.

app.js

import { PrismaClient, Prisma } from '@prisma/client';

// ...

function asyncHandler(handler) {
  return async function (req, res) {
    try {
      await handler(req, res);
    } catch (e) {
      if (
        e instanceof Prisma.PrismaClientValidationError ||
        e.name === 'StructError'
      ) {
        res.status(400).send({ message: e.message });
      } else if (
        e instanceof Prisma.PrismaClientKnownRequestError &&
        e.code === 'P2025'
      ) {
        res.sendStatus(404);
      } else {
        res.status(500).send({ message: e.message });
      }
    }
  };
}

// ...

app.post('/users', asyncHandler(async (req, res) => {
  assert(req.body, CreateUser); 
  // ...
}));
  • e.name === 'StructError': Superstruct 객체와 형식이 다를 경우 발생
  • e instanceof Prisma.PrismaClientValidationError: 데이터를 저장할 때 모델에 정의된 형식과 다른 경우 발생 (Superstruct로 철저히 검사하면 이 상황은 잘 발생하지 않지만 안전성을 위해 둘 다 검사)
  • e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025': 객체를 찾을 수 없을 경우 발생

Primary key: RDB에서 어떤 컬럼을 구분하는 기준이 된다. 유니크하다.

Foreign key: RDB에서 어떤 테이블이 다른 테이블을 참조할 때 기준이 된다. 다른 테이블의 PK를 사용한다.

ER 모델: 개체, 속성, 관계로 데이터를 표현하는 방식.

각각의 개체는 여러 속성을 가지고, 각 개체는 특정한 관계를 가지기도 한다.

각 사각형이 개체, 개체 안의 여러가지가 속성에 해당하며, 개체 사이의 선이 관계를 나타낸다.

관계선에 표시된 모양들은 카디널리티로, 각 개체가 서로 최대 몇개의 관계가 가능한가를 나타낸다.

1:1, 1:N, M:N 관계가 존재한다.

수직선은 관계 1을, 삼각선은 관계 N을 나타낸다.

따라서 UserPreference - User는 1:1관계, Order - OrderItem은 1:N 관계, User - Product는 M:N관계이다.

더 안쪽에 있는 모양은 최소 카디널리티를 나타내며, 원형이 0을 나타낸다.

따라서 User가 존재하더라도 Order는 0개 존재할 수 있으며, Order가 존재한다면 User는 1개 이상 존재해야한다.


관계 또한 schema.prisma 파일에 적용한다.

model User {
  // ...

  // @@unique([firstName, lastName]) // 여러 필드의 조합 결과가 유니크해야할 경우 이와 같이 선언한다.
  // 1대 다 관계에서 '1'측에선 '다'측 배열을 정의한다. 실제 db에는 저장되지 않는다.
  orders         Order[]
  userPreference UserPreference?
  savedProducts  Product[]
}

model Product {
  //...

  orderItems OrderItem[]
  savedUsers User[]
}

model UserPreference {
  //...

  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId String @unique
}

model Order {
  // ...

  // 1대 다 관계에선 '다'측에 FK와 관계를 정의한다. 실제로 저장되는건 FK뿐이다.
  // 관계를 정의할 때 타 모델의 타입까지만 선언한 뒤 format해주면 자동으로 삽입된다. 이 경우엔 user User까지만 선언하고 format해주면 된다.
  user       User?       @relation(fields: [userId], references: [id], onDelete: SetNull)
  userId     String?
  orderItems OrderItem[]
}

model OrderItem {
  //...

  order     Order    @relation(fields: [orderId], references: [id], onDelete: Cascade)
  product   Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
  orderId   String
  productId String?
}

1:N 관계(User-Order)에선 N측에 단일 키를, 1측에 N측의 List타입을 선언한다.

M:N 관계(User-Product)에선 각 측에 List타입을 선언한다.

1:1 관계(User-Userpreference)는 각 측에 단일 키를 선언한다.

onDelete는 FK가 삭제될 때 어떻게 행동할지를 결정한다.

Cascade: 해당 키를 가진 개체가 삭제될 경우 이 컬럼도 삭제한다.

SetNull: 이 키를 Null로 한다. Nullable 해야 한다.

SetDefault: 이 키의 Default값으로 변경한다. @default를 설정해야한다.

Restrict: 이 FK가 설정되어 있으면 이 FK를 PK로 가지는 컬럼은 삭제할 수 없다.


가져온 데이터를 가공하거나, 가져올 때 조건부로 가공할 수 있다.

includes 옵션을 통해 FK가 설정된 테이블의 데이터를 묶어서 가져올 수 있다.

select 옵션을 통해 특정 필드만 가져올 수 있다.

가져온 데이터는 js 객체이므로, 해당 데이터를 통해 새로운 데이터, 필드 등을 계산하고 객체에 담아서 보여줄 수 있다.

데이터를 생성할 때 관계가 설정된 테이블의 데이터도 함께 생성할 수 있다.

const { userPreference, ...userFields } = req.body;
const user = await prisma.user.create({
  data: { ...userFields, userPreference: { create: userPreference } },
  include: { userPreference: true },
});

이미 존재하는 데이터 사이에 관계를 설정할 수도 있다.

const { id: userId } = req.params;
const { productId } = req.body;

// 이미 찜해뒀는지 확인
const user = await prisma.user.findUnique({
  where: { id: userId },
  include: {
    savedProducts: true,
  },
});
const isSaved = user.savedProducts.some((p) => p.id === productId);
const option = isSaved
  ? { disconnect: { id: productId } }
  : { connect: { id: productId } };

// 찜해뒀으면 삭제, 아니면 추가
const { savedProducts } = await prisma.user.update({
  where: { id: userId },
  data: { savedProducts: option },
  include: {
    savedProducts: true,
  },
});
res.send(savedProducts);
})

$transaction 메소드를 통해 여러 비동기 처리를 한꺼번에 처리할 수 있다. 하나라도 실패하면 전부 드랍하기 때문에 일부 작업만 누락하는 것을 방지한다.

// transaction으로 한번에 처리, 하나라도 오류가 발생할 경우 전체 드랍
const [order] = await prisma.$transaction([
  createOrderQuery,
  ...decrementStocksQueries,
]);

더보기

관계 정의하기

일대다 관계

일대다 관계는 다음과 같이 정의합니다.

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와 관계 필드를 옵셔널(?)하게 만드는 것입니다. Foreign key가 있는 모델 반대쪽의 최소 카디널리티가 1이라면 foreign key와 관계 필드를 필수로 만들고 최소 카디널리티가 0이라면 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 Fs 2nd > 강의내용정리' 카테고리의 다른 글

백엔드 기초  (0) 2024.09.10
React-Router  (2) 2024.09.10
React로 데이터 다루기  (2) 2024.09.10
React 기초  (0) 2024.09.10
CodeIt Sprint FS 2nd 18~20일차  (0) 2024.08.26