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

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

by wonee1 2024. 7. 3.
728x90

 

 

<관계형 데이터베이스를 활용한 자바스크립트 서버 만들기>

 

 

 

 

💡저장하려는 데이터의 명확한 구조가 있고 데이터 간의 관계가 여러 개 있을 때 관계형 데이터베이스를 주로 사용한다

 

 

DBMS(Database Management System)

 

 

 

javascript의 ORM 라이브러리를 사용하면 데이터베이스와 소통할 수 있고 객체처럼 사용할 수 있다

 


 

Prisma 기본기

Prisma 초기화

 

prisma는 npx로 시작한다

→ npx prisma init -- datasource-provider postgresql

 

 

.env는 postgreSQL에 접속하기 위한 환경변수 파일

 

필요한 테이블구조를 정의하는 파일

 

 

User 모델 만들기 

 

 

model이란 데이터베이스 테이블을 나타낸다

필드 이름 필트 타입으로 코드를 작성한다

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
}

• model은 적어도 하나의 유니크 필드가 필요하다

• @ 로 필드의 추가정보를 명시한다

 

 

 

Prisma Schema 추가 기능

 

 

enum

 

•필드의 값이 몇 가지 정해진 값 중 하나일 때는 enum(enumerated type)을 사용할 수 있다.

model User {
  // ...
  membership  Membership  @default(BASIC)
}

enum Membership {
  BASIC
  PREMIUM
}

• enum 값은 보통 대문자를 사용하고 필드의 타입을 enum 이름으로 설정하면 된다.

•@default 어트리뷰트도 사용할 수 있다.

• enum은 서로 연관된 상수들의 집합

• SQLite에서는 enum을 사용할 수 없다

 

 

 

 

@@unique

 

•여러 필드의 조합이 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])
}

 

•위처럼 정의하면 firstName과 lastName의 조합이 unique해야 한다.

 

// 삽입 가능

{
  id: "abc",
  firstName: "길동",
  lastName: "홍",
  // ...
},
{
  id: "def",
  firstName: "길동",
  lastName: "박",
  // ...
}

 

 

 

 

마이그레이션

 

💡model코드를 데이터베이스에 반영하는 과정을 마이그레이션이라고 한다

→ npx prisma migrate dev 으로 마이그레이션

 

 

schema prisma에 있던 코드를 sql문으로 바꿔준다

 

 

npx prisma studio로 접속해서 데이터를 입력한다

 

 

테이블에 데이터가 있을 때 마이그레이션하기

 

-- CreateEnum
CREATE TYPE "Category" AS ENUM ('FASHION', 'BEAUTY', 'SPORTS', 'ELECTRONICS', 'HOME_INTERIOR', 'HOUSEHOLD_SUPPLIES', 'KITCHENWARE');

-- AlterTable
ALTER TABLE "User" ADD COLUMN     "age" INTEGER;

-- CreateTable
CREATE TABLE "Product" (
    "id" TEXT NOT NULL,
    "name" TEXT NOT NULL,
    "description" TEXT,
    "category" "Category" NOT NULL,
    "price" DOUBLE PRECISION NOT NULL,
    "stock" INTEGER NOT NULL,
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
    "updatedAt" TIMESTAMP(3) NOT NULL,

    CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
);

 

 

 

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
}

enum Category {
  FASHION
  BEAUTY
  SPORTS
  ELECTRONICS
  HOME_INTERIOR
  HOUSEHOLD_SUPPLIES
  KITCHENWARE
}

 

 

 

 

Prisma Client와 데이터베이스 CRUD I

 

prisma client는 모델 정보를 저장하고 있다

 

 

client 사용법

import { PrismaClient } from '@prisma/client';//client를 불러오는 코드 
...
...

app.get('/users', async (req, res) => {
    const users = await prisma.user.findMany(); //어떤 모델과 상호작용하려면 prisma.모델이름
    //모델이름은 schema.prisma 파일에 정의된 모델의 이름을 참조
    res.send(users);
});

app.get('/users/:id', async (req, res) => {
    const { id } = req.params;
    const user = await prisma.user.findUnique({//고유필드로 데이터하나를 찾을 때 findUnique 사용
        where: { id },//where로 결과를 필터
    });//id에 해당하는 유저 조회
    res.send(user);
});

 

 

Prisma에서 모델과 상호작용할 때 사용할 수 있는 주요 메서드

  • findMany: 여러 레코드를 조회
  • findUnique: 고유한 단일 레코드를 조회
  • create: 새로운 레코드를 생성
  • update: 기존 레코드를 업데이트
  • delete: 기존 레코드를 삭제.
  • upsert: 레코드를 생성하거나 업데이트
  • findFirst: 첫 번째 레코드를 조회
  • count: 특정 조건을 만족하는 레코드의 수를 셈

 

 

Prisma Client와 데이터베이스 CRUD II

 

app.post('/users', async (req, res) => {
  // 리퀘스트 바디 내용으로 유저 생성-> create 메소드 사용
  const user = await prisma.user.create({
    data:req.body,

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

app.patch('/users/:id', async (req, res) => {
  const { id } = req.params;
  // 리퀘스트 바디 내용으로 id에 해당하는 유저 수정 ->update메소드 사용
  const user = await prisma.user.update({
    where: {id }, // where id를 넘겨준다
    data: req.body, //data로 req.body를 넘겨준다
  });
  res.send(user);
});

app.delete('/users/:id', async (req, res) => {
  const { id } = req.params;
  // id에 해당하는 유저 삭제
  await prisma.user.delete({
    where: {id},
  });

  res.sendStatus(204);
});

 

 

Product CRUD API 만들기

 

import * as dotenv from 'dotenv';
dotenv.config();
import express from 'express';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

const app = express();
app.use(express.json());

/*********** users ***********/

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

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

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

/*********** products ***********/

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

app.get('/products/:id', async (req, res) => {
  const { id } = req.params;
  const product = await prisma.product.findUnique({
    where: { id },
  });
  res.send(product);
});

app.post('/products', async (req, res) => {
  const product = await prisma.product.create({
    data: req.body,
  });
  res.status(201).send(product);
});

app.patch('/products/:id', async (req, res) => {
  const { id } = req.params;
  const product = await prisma.product.update({
    where: { id },
    data: req.body,
  });
  res.send(product);
});

app.delete('/products/:id', async (req, res) => {
  const { id } = req.params;
  await prisma.product.delete({
    where: { id },
  });
  res.sendStatus(204);
});

app.listen(process.env.PORT || 3000, () => console.log('Server Started'));

 

 

 

데이터베이스 시딩

 

mock 데이터를 시드 커맨드를 실행해서 데이터 베이스에 시딩한다

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() //main함수를 안전하게 실행하는 코드-> 코드가 실행 된 후 데이터베이스와의 연결을 종료
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

package.json 수정

{
  "dependencies": {
    "@prisma/client": "^5.4.2",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "is-email": "^1.0.2",
    "is-uuid": "^1.0.2",
    "prisma": "^5.4.2",
    "superstruct": "^1.0.3"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  },
  "type": "module",
  "scripts": {
    "dev": "nodemon app.js",
    "start": "node app.js"
  },
  "prisma":{
    "seed":"node prisma/seed.js"
  }
}

npx prisma db seed 명령어로 실행한다

그 후 prisma studio를 실행시킨다

 

 

 

 

 

 

쿼리 파라미터 처리하기

 

 

offset은 데이터 몇 개를 건너뛸 것인지 결정한다

limit과 offset을 결과를 페이지처럼 나눠서 가져오는 기능을 제공할 수 있다

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: {createdAt:'asc'},//오름차순 정렬 
      orderBy,
      skip: parseInt(offset), //skip 프로퍼티로 설정, 숫자로 할당
      take: parseInt(limit), // take 프로퍼티로 설정, 숫자로 할당 
      
    }); //어떤 모델과 상호작용하려면 prisma.모델이름
    ////모델이름은 schema.prisma 파일에 정의된 모델의 이름을 참조

    res.send(users);
});

 

 

http

GET <http://localhost:3000/users?order=oldest>

 

 

 

Prisma Client 추가 기능

 

 

추가적인 CRUD 메소드

 

.findFirst()

• id로 객체 하나를 조회할 때는 .findUnique() 메소드를 사용했음.

•.findUnique() 메소드는 id 필드나 unique한 필드로만 필터를 할 수 있습니다.

// OK
const user = await prisma.user.findUnique({
  where: { id: 'b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e' },
});

// 오류(firstName은 unique한 필드가 아님)
const user = await prisma.user.findUnique({
  where: { firstName: '길동' },
});

💡Unique하지 않은 필드로 필터를 해서 객체 하나를 조회하고 싶을 때는 .findFirst() 메소드를 사용하면 됩니다.

const user = await prisma.user.findFirst({
  where: { firstName: '길동' },
});

console.log(user);

{
  "id": "b8f11e76-0a9e-4b3f-bccf-8d9b4fbf331e",
  "email": "honggd@example.com",
  "firstName": "길동",
  "lastName": "홍",
  "address": "서울특별시 강남구 무실로 123번길 45-6",
  "createdAt": "2023-07-16T09:00:00.000Z",
  "updatedAt": "2023-07-16T09:00:00.000Z"
}

• where 조건을 만족하는 결과가 여러 개 있을 때 첫 번째 객체를 리턴하다.

데이터베이스가 사용하는 순서를 바꾸고 싶다면 orderBy 프로퍼티를 사용하면 된다.

 

 

.upsert()

•.upsert() 메소드는 where 조건을 만족하는 객체가 있다면 객체를 업데이트(update) 하고, 없다면 생성(insert) 한다( Update와 insert를 합친 메소드)

const user = await prisma.user.upsert({
  where: { email: 'yjkim@example.com' },
  data: {
    email: 'yjkim@example.com',
    firstName: '유진',
    lastName: '김',
    address: '충청북도 청주시 북문로 210번길 5',
  },
});

 

 

.count()

• 객체의 개수만 필요한 경우 .count() 메소드를 사용하면 된다.

• .findMany()를 사용해서 배열의 길이를 사용하는 것보다 더 효율적

const count = await prisma.product.count({
  where: {
    category: 'FASHION',
  },
});

console.log(count);

16

 

 

필터 조건

•where 프로퍼티에 필드 이름과 값을 전달하면 기본적으로 일치(equal) 연산자가 활용된다

// category가 'FASHION'인 Product들 필터
const products = await prisma.product.findMany({
  where: {
    category: 'FASHION',
  },
});

이 외에도 not, in, contains, startsWith 같은 다양한 비교 연산자를 사용할 수 있다

// category가 'FASHION'이 아닌 Product들 필터
const products = await prisma.product.findMany({
  where: {
    category: {
      not: 'FASHION',
    },
  },
});

// category가 'FASHION', 'SPORTS', 'BEAUTY' 중 하나인 Product들 필터
const products = await prisma.product.findMany({
  where: {
    category: {
      in: ['FASHION', 'SPORTS', 'BEAUTY']
    },
  },
});

// name에 'TV'가 들어가는 Product들 필터
const products = await prisma.product.findMany({
  where: {
    name: {
      contains: 'TV',
    },
  },
});

// name이 'Apple'로 시작하는 Product들 필터
const products = await prisma.product.findMany({
  where: {
    name: {
      startsWith: 'Apple',
    },
  },
});

그리고 여러 필터를 조합할 수도 있다

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

 

 

 

유효성 검사

 

 

Superstruct Types 라이브러리를 사용해서 유효성 검사를 한다

 

 

import * as s from 'superstruct';
import isEmail from 'is-email';

//유효성 검사는 수퍼스트럭트로 예상하는 데이터 형식을 정의하고
// 실제 받은 데이터가 형식과 일치하는지 확인 

export const CreateUser= s.object({
    email:s.define('Email',isEmail),//email타입은 isEmail형식을 통과해야한다
    firstName:s.size(string(),1,30),//size 함수는 string 타입의 길이를 제한 
    lastName:s.size(string(),1,30),
    address:s.string(),
});

export const PatchUser=s.partial(CreateUser);// CreateUser의 일부면 ok

 

 

app.js 파일을 수정

import { assert } from 'superstruct';
import { CreateUser , PatchUser} from './struct';
app.post('/users', async (req, res) => {
  assert(req.body,CreateUser); // 확인하고자하는 데이터 객체와 수퍼스트럭트 객체를 전달하면 된다 (유효성 검사)
  // 리퀘스트 바디 내용으로 유저 생성-> create 메소드 사용
  const user = await prisma.user.create({
    data:req.body,

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

app.patch('/users/:id', async (req, res) => {
  assert(req.body,PatchUser); 
  const { id } = req.params;
  // 리퀘스트 바디 내용으로 id에 해당하는 유저 수정 ->update메소드 사용
  const user = await prisma.user.update({
    where: {id }, // where id를 넘겨준다
    data: req.body, //data로 req.body를 넘겨준다
  });
  res.send(user);
});

 

 

728x90