HJW's IT Blog

[CodeIt] 07월 3주차 본문

카테고리 없음

[CodeIt] 07월 3주차

kiki1875 2024. 7. 18. 16:25

Prisma 설치 / 초기화

npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider db-type // qostgresql, mysql, sqlite 등
  • Prisma Schema 생성
// 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
}


npx prisma init --datasource-provider postgresql
  • 초기화 및 시작
// .env

DATABASE_URL="postgresql://postgres:gjwldnd!1@localhost:5432/comazon_dev?schema=public"
PORT=3000
  • schema.prisma 에서 model 은 table 이라 보면 된다
  • Shift + Alt + F 로 자동 줄맞춤
model User {
  id        String @id @default(uuid())
  email     String @unique
  firstName String
  lastName  String
  address   String?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
  • @id 는 unique 한 식별자임을 나타낸다
  • @unique 는 유일한 값임을 나타낸다
  • ? 는 optional 한 필드임을 나타낸다
  • @default(uuid()) : 36자리 id
  • 다음과 같이 enum type 을 사용할 수도 있다
model User {
  // ...
  membership  Membership  @default(BASIC)
}

enum Membership {
  BASIC
  PREMIUM
}

  • multivalued + 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])
}
// fname, lname 의 조합이 unique 해야 한다
  • 마이그레이션 : 모델코드를 DB 에 반영하는 과정으로, 스키마 파일에 변동사항이 있다면 항상 해주어야 한다
npx prisma migrate dev
npx prisma studio
  • 위 명령어로 db gui 에 접근할 수 있다
  • 테이블에 이미 데이터가 있어 migration 을 못할 경우, 우선 nullable 필드로 생성한 후, 데이터를 넣고 추가로 migration 을 해주면 된다
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
  • 위 코드를 통해 PrismaClient import
// get users

app.get('/users', async (req, res) => {
  const users = await prisma.user.findMany();
  res.send(users);
});

//get user by 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 uer = await prisma.user.create({
    data: req.body,
  });

  res.status(201).send(user);
});

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

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

// package.json 에 추가
  "prisma":{
    "seed": "node prisma/seed.js"
  }
npx prisma db seed

쿼리 파라미터

app.get('/users', async (req, res) => {
  const { offset = 0, limit = 10, order = 'newest'} = req.query;
  const users = await prisma.user.findMany({
	  orderBy: {createdAt: 'asc'},
    skip : parseInt(offset),
    take : parseInt(limit),
  });
  res.send(users);
});

offset : 몇개의 데이터를 건너뛸지

limit :

orderBy: { 정렬 필드, 순서}

추가 CRUD Method

.findFirst()

  • unique 하지 않은 필드 사용시

.upsert()

  • where 를 만족하는 객체를 update, 없다면 create

.count()

  • 객체의 개수만 반환

유효성 검사

//structs.js

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

export const PatchUser = s.partial(CreateUser);
//app.js 에 추가
import { assert } from 'superstruct'
import { CreateUser } from './structs.js' 
app.post('/users', async (req, res) => { 
  // 리퀘스트 바디 내용으로 유저 생성
  assert(req.body, CreateUser); // 유효성 검사
  const user = await prisma.user.create({
    data: req.body,
  });
 
  res.status(201).send(user); 
});

  • express 에서 오류가 발생하면 서버가 죽기 때문에 try catch 로 감싸주어야 한다
  • 유효성 검사 asyncHander 에서 error.name 은 StructError 이다

RDB 기본기

  • Primary Key : row 를 식별하는 unique value column. 다른 row 로도 식별은 가능하다.
  • Foreign Key : Relationship 이 있는 다른 테이블의 PK
  • 데이터 모델링 : 서비스에 정확히 어떤 데이터가 필요하고, 데이터간 어떤 관꼐가 있는지를 정교하게 표현하는 것
  • ER (Entity Relationship) Model
    • Entity : 현실세계의 사물 혹은 객체. 하나의 개체 = 하나의 테이블
    • Attribute : Entity 의 세부 정보
    • Relationship : Entity 간의 관계'
  • Cardinality
    • 1 : 1 → 하나의 entity 가 정확하 하나의 다른 entity instance와 관계
    • 1 : N → 하나의 entity 가 최대 N개의 다른 entity instance와 관계
    • N : M → N개의 entity instance 가 최대 M개의 다른 entity instance 와 관계

 

Prisma에서 Relationship 정의

1:N 관계

model User {
  id        String   @id @default(uuid())
  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
}
  • Order model 에 user User 과 같이 선언한 뒤 alt shift f 로 자동완성

1:1 관계

model User {
  id             String           @id @default(uuid())
  email          String           @unique
  firstName      String
  lastName       String
  address        String
  createdAt      DateTime         @default(now())
  updatedAt      DateTime         @updatedAt
  orders         Order[]
  UserPreference UserPreference?
}

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
}

N:M 관계

model User {
  id             String           @id @default(uuid())
  email          String           @unique
  firstName      String
  lastName       String
  address        String
  createdAt      DateTime         @default(now())
  updatedAt      DateTime         @updatedAt
  orders         Order[]
  UserPreference UserPreference?
  savedProducts 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
  savedUsers User[]
}
  • onDelete option
    • 연결된 데이터가 삭제되었을때 기존 데이터를 어떻게 처리할 것인지
    • Cascade : FK 가 가르키는 데이터 삭제시, 기존 데이터도 삭제
    • Restrict : 특정 데이터를 참조한느 데이터들이 있으면 데이터를 삭제하지 못함
    • SetNull : FK 가 가르키는 데이터 삭제시 NULL 로 설정
    • SetDefault : 삭제시 FK 를 기본 default 값으로 설정
model UserPreference {
  id           String   @id @default(uuid())
  receiveEmail Boolean
  createdAt    DateTime @default(now())
  updatedAt    DateTime @updatedAt
  user         User     @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId       String   @unique
}
  • user Preference 도 get route 에서 가져오기
app.get('/users/:id', asyncHandler(async (req, res) => {
  const { id } = req.params;
  const user = await prisma.user.findUniqueOrThrow({
    where: { id },
    include: {
      userPreference: true,
    },
  });
  res.send(user);
}));
  • 특정 필드만 가져오려면
include: {
      userPreference: {
        select: {
          receiveEmail: true,
        },
      },
    },
  • 관련 객체 생성
//user.http

POST <http://localhost:3000/users>
Content-Type: application/json

{
  "email": "yjkim@gmail.com", 
  "firstName": "유진",
  "lastName": "김a",
  "address": "충청북도 청주시 북문로 210번길 5",
  "userPreference":{
    "receiveEmail": false
  }
}

//struct.js
app.post(
  '/users',
  asyncHandler(async (req, res) => {
    assert(req.body, CreateUser);
    const { userPreference, ...userFields} = req.body;
    const user = await prisma.user.create({
      data: req.body,
    });
    res.status(201).send(user);
  })
);

//app.js
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);
  })
);

//app.js / patch

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);
  })
);
  • 관련된 객체는 꼭 create property 를 이용해야 한다.
  • 객체 연결
//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: {
            id: productId,
          },
        },
      },
      include: {
        savedProducts: true,
      },
    });
    res.send(savedProducts);
  })
);

// struct.js
export const PostSavedProduct = s.object({
  productId: Uuid,
})
  • user 를 해당 id 를 가진 상품과 연결하라는 뜻이다
  • 기존에 존재하는 객체와 연결하기 위해서는 → struct.js 에 정의 → 유효성 검사 → update → data 내에 조건에 따른 id 선언
  • 제고 관리
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 order = await prisma.order.create({
      data: {
        userId,
        orderItems: {
          create: orderItems,
        },
      },
      include: {
        orderItems: true,
      },
    });

    const queries = productIds.map((productId) =>
      prisma.product.update({
        where: { id: productId },
        data: { stock: { decrement: getQuantity(productId) } },
      })
    );

    await Promise.all(queries);

    res.status(201).send(order);
  })
);
  • transaction
    • await prisma.$transaction();
    • 여러 쿼리를 묶어서 실행한다
      • 모든 작업이 성공하거나 모든 작업이 실패
const [order] = await prisma.$transaction(
      [
        prisma.order.create({
          data: {
            userId,
            orderItems: {
              create: orderItems,
            },
          },
          include: {
            orderItems: true,
          },
        }),
        ...queries,
      ]
    );
  • 안에 실행할 쿼리들을 배열 형태로 넘겨주면 된다
  • 즉, 일부만 성공하고 일부는 실패하는 일이 없다