DB | [ORM] Prisma

bubblegum·2024년 1월 26일
0

DB

목록 보기
10/11
post-thumbnail

Prisma

mongoose가 ODM(Object Document Mapping)으로 Javascript의 객체와 비관계형 데이터베이스인 MongoDB를 연결하는 것처럼, Prisma는 ORM(Object Relational Mapping)으로 Javascript의 객체와 MySQL, Oracle, MariaDB, PostgreSQL와 같은 관계형 데이터베이스를 연결해주는 라이브러리이다.

Prisma와 같은 ORM을 사용하는 가장 큰 이유 2가지는 "프로덕션에서 사용하는 데이터베이스를 변경할 때 ORM의 속성값을 이용하면 Raw Query보다 간편하다"는 점과 "데이터베이스에서 사용하는 DB 또는 Table 속성이 변경되었을 때 빠르게 수정이 가능하다"는 것이다.

터미널로 Prisma 라이브러리 설치하기

# yarn 프로젝트를 초기화합니다.
yarn init -y

# express, prisma, @prisma/client 라이브러리를 설치합니다.
yarn add express prisma @prisma/client

# nodemon 라이브러리를 DevDependency로 설치합니다.
yarn add -D nodemon

# 설치한 prisma를 초기화 하여, prisma를 사용할 수 있는 구조를 생성합니다.
npx prisma init

명령어 nodemon

nodemon은 파일을 저장할 때마다 변경 사항을 감지하고, 자동으로 서버를 재시작해주는 라이브러리이다. 개발 중 변경사항을 즉시 반영하여 개발 효율성을 향상시킬 수 있다. nodemon을 실행 시키는 방법은 두 가지 이다. 먼저 nodemon 뒤에 바로 실행할 파일명을 적는 방법이 있다.

nodemon your_server_file.js #첫 번째 방법

그 다음으로는 package.json에 nodemon을 이용하여 서버를 실행하는 스크립트(scripts)를 등록하는 방법이 있다.

// package.json

"scripts": {
	"dev": "nodemon app.js" //스크립트에 등록
},
#터미널 실행 명령어
nodemon dev 

명령어 npx prisma init

npx prisma init 명령어를 입력하면 다음과 같은 폴더 구조를 생성한다.

내 프로젝트 폴더 이름
├── prisma
│   └── schema.prisma
├── .env
├── .gitignore
├── package.json
└── yarn.lock

schema.prisma 파일

Prisma는 연결하려는 데이터베이스의 속성을 schema.prisma 파일에서 관리하고 있다. 맨 처음에 schema.prisma 파일을 확인하면 아래의 2가지 구문이 작성되어 있는 것을 확인할 수 있다.

  • generator 구문
    Prisma 클라이언트를 생성하는 방식을 설정한다.
  • datasource 구문
    프로퍼티를 수정하여 사용자 아이디, 비밀번호, 엔드 포인트 등 다양한 설정값을 입력할 수 있다.
// 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 = "mysql"  
  url      = env("DATABASE_URL")
}
  • model 구문
    model 구문은 특정 Table과 Column의 속성값을 입력하여, 데이터베이스와 Express 프로젝트를 연결(Mapping)시켜주는 역할을 한다.
model Products {
  productId   Int     @id @default(autoincrement()) @map("productId")
  productName String  @unique @map("productName") // unique는 1:1 관계를 유지함 
  price       Int     @default(1000) @map("price")
  info        String? @map("info") @db.Text

  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Products")
}

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  title     String   @map("title")
  content   String   @map("content") @db.Text
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  @@map("Posts")
}
  • 데이터 유형: 각 필드의 데이터를 어떤 형식으로 저장할 것인지 결정한다. 위 예시에서는 Int, String, DateTime 등의 데이터 유형이 사용되었습니다. 데이터 유형 뒤에 ?가 붙게 된다면, NULL을 허용한다는 뜻이다. 이 문법은 TypeScript에서 Optional Parateters 라고 불린다.
  • 제약조건: UNIQUE 제약조건과 AUTO_INCREMENT 제약조건을 위와 같이 사용할 수 있다.
  • @@map(): @@map("Products")Products 테이블을 MySQL에서도 Products란 이름으로 사용하겠다는 뜻이다. @@map() 을 작성하지 않으면, 테이블명의 대문자는 전부 소문자로 치환된다.

datasource의 url

아이디와 비밀번호를 숨기기 위해 DB URL은 .env 파일에 숨겨지게 되는데 DB URL의 형식은 다음과 같다.

# .env

DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

데이터베이스 URL의 구조는 다음과 같이 나눌 수 있다.

  • Protocol: Prisma가 사용할 데이터베이스 엔진
  • Base URL: 데이터베이스의 엔드 포인트와 아이디, 패스워드, 포트 번호
    <Id>:<Password>@<RDS Endpoint>:<Port>
  • Path: DB 엔진에서 사용할 데이터베이스 이름을 설정
  • Arguments: Prisma에서 데이터베이스 연결을 설정하는데 필요한 추가 옵션

Prisma Client

Prisma Client는 Prisma Schema에 정의한 데이터베이스 모델을 TypeScript 코드로 변환하여, 개발자가 데이터베이스와 상호작용할 수 있게 해주는데요. 이러한 과정을 통해, 데이터베이스를 JavaScript에서 손쉽게 다룰 수 있게 되고, Prisma Schema와 동기화된 Prisma Client를 이용해 데이터베이스를 사용할 수 있게 되는 것입니다.

// node_modules/.prisma/client/index.d.ts

export type ProductsPayload<ExtArgs extends $Extensions.Args = $Extensions.DefaultArgs> = {
  name: "Products"
  objects: {}
  scalars: $Extensions.GetResult<{
    productId: number
    productName: string
    price: number
    info: string | null
    createdAt: Date
    updatedAt: Date
  }, ExtArgs["result"]["products"]>
  composites: {}
}

export type Products = runtime.Types.DefaultSelection<ProductsPayload>

Prisma 관계 표현법

// schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Users {
  userId    Int      @id @default(autoincrement()) @map("userId")
  email     String   @unique @map("email")
  password  String   @map("password")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  userInfos UserInfos? // 사용자(Users) 테이블과 사용자 정보(UserInfos) 테이블이 1:1 관계를 맺습니다.
  posts     Posts[] // 사용자(Users) 테이블과 게시글(Posts) 테이블이 1:N 관계를 맺습니다.
  comments  Comments[] // 사용자(Users) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.

  @@map("Users")
}

model Posts {
  postId    Int      @id @default(autoincrement()) @map("postId")
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  title     String   @map("title")
  content   String   @map("content") @db.Text
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user     Users      @relation(fields: [userId], references: [userId], onDelete: Cascade)
  comments Comments[] // 게시글(Posts) 테이블과 댓글(Comments) 테이블이 1:N 관계를 맺습니다.

  @@map("Posts")
}

model UserInfos {
  userInfoId   Int      @id @default(autoincrement()) @map("userInfoId")
  userId       Int      @unique @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  name         String   @map("name")
  age          Int?     @map("age")
  gender       String   @map("gender")
  profileImage String?  @map("profileImage")
  createdAt    DateTime @default(now()) @map("createdAt")
  updatedAt    DateTime @updatedAt @map("updatedAt")

  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("UserInfos")
}

model Comments {
  commentId Int      @id @default(autoincrement()) @map("commentId")
  postId    Int      @map("postId") // 게시글(Posts) 테이블을 참조하는 외래키
  userId    Int      @map("userId") // 사용자(Users) 테이블을 참조하는 외래키
  content   String   @map("content")
  createdAt DateTime @default(now()) @map("createdAt")
  updatedAt DateTime @updatedAt @map("updatedAt")

  // Posts 테이블과 관계를 설정합니다.
  post Posts @relation(fields: [postId], references: [postId], onDelete: Cascade)
  // Users 테이블과 관계를 설정합니다.
  user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)

  @@map("Comments")
}

Prisma 1:1 연관 관계

사용자(Users) 모델은 사용자 정보(UserInfos) 모델과 1:1 관계를 가지고 있습니다. 1:1 관계란 한 사용자가 하나의 사용자 정보만 가질 수 있고, 한 사용자 정보는 한 사용자에게만 속할 수 있다는 것을 의미합니다. 1:1 관계를 설정할 때는 다음과 같은 내용을 포함해야 합니다.

  1. 관계를 설정하려는 모델(UserInfos)에서 어떤 모델과 관계를 맺을지(Users) 설정해야합니다.
  2. 관계를 맺게되는 모델(Users)에서 어떤 모델이 관계를 맺는지(UserInfos) 설정해야합니다.
  3. 관계를 맺게되는 모델(Users)에서, 타입을 지정할 때 Optional Parameter(?)를 지정해줘야합니다. 사용자는 사용자 정보가 존재하지 않을 수 있기 때문이죠.
  • Users : 참조할 모델을 지정합니다. 사용자(Users) 모델을 참조하므로 Users로 작성되어있습니다.
  • fields : 사용자 정보(UserInfos) 모델에서 사용할 '외래 키(Foreign Key)' 컬럼을 지정합니다. 여기선, userId 컬럼으로 외래 키를 지정하였습니다.
  • references : 참조하는 다른 모델의 컬럼(Column)를 지정합니다. 여기선, 사용자(Users) 모델의 userId 컬럼을 참조합니다.
  • onDelete | onUpdate : 참조하는 모델이 삭제 or 수정될 경우 어떤 행위를 할 지 설정합니다. Cascade 옵션을 선택하여 사용자가 삭제될 경우 그에 연결된 사용자 정보도 함께 삭제되도록 설정하였습니다.

Prisma 1:N 연관 관계

1:N 관계란 한 사용자는 여러개의 게시글을 작성할 수 있다는 것을 의미합니다. 여기서 사용자(Users) 모델과 게시글(Posts) 모델은 1:N 관계를 가지고 있습니다. 이런 1:N 관계를 설정할 때는 다음과 같은 내용을 포함해야 합니다.

  1. 관계를 설정하려는 모델(Posts)에서 어떤 모델과 관계를 맺을지(Users) 설정해야합니다.
  2. 관계를 맺게되는 모델(Users)에서 어떤 모델이 관계를 맺는지(Posts) 설정해야합니다.
  3. 관계를 맺게되는 모델(Users)에서, 타입을 지정할 때, 배열 연산자([])를 작성해줘야합니다. 사용자는, 여러개의 게시글을 가질 수 있기 때문이죠.

현재 게시글 모델의 경우 작성한 사용자가 회원 탈퇴(onDelete)하게 될 경우 작성한 모든 게시글이 삭제되도록 구현되어 있습니다. 이런 설정은 @relation 어노테이션을 사용하여 지정합니다.

// Users 테이블과 관계를 설정합니다.
user     Users     @relation(fields: [userId], references: [userId], onDelete: Cascade)

여기서 User는 게시글(Posts)이 참조하는 다른 모델을 지정하고, fields는 게시글(Posts) 모델에서 사용할 외래키 컬럼을 지정합니다. references는 참조하는 다른 모델의 컬럼을 지정하고, onDelete는 잠조하는 모델이 삭제될 경우 어떤 행위를 할 지 설정합니다. onDelete의 경우, Cascade 옵션으로 사용자가 삭제될 경우 연관된 게시글 또한 삭제되도록 설정하였습니다.

Prisma Method

Prisma는 mongoose와 동일하게, findMany(), findFirst(), findUnique() 등 다양한 메서드를 지원한다. mongoose를 사용했을 때는 Schema를 이용해 DB를 사용하였다면, Prisma에서는 Prisma Client를 이용해 MySQL의 데이터를 조작할 수 있다.

// routes/posts.router.js
        
import express from 'express';
import { PrismaClient } from '@prisma/client';

// express.Router()를 이용해 라우터를 생성합니다.        
const router = express.Router(); 
const prisma = new PrismaClient({
  // Prisma를 이용해 데이터베이스를 접근할 때, SQL을 출력해줍니다.
  log: ['query', 'info', 'warn', 'error'],

  // 에러 메시지를 평문이 아닌, 개발자가 읽기 쉬운 형태로 출력해줍니다.
  errorFormat: 'pretty',
}); // PrismaClient 인스턴스를 생성합니다.
        
export default router;
// app.js
        
import express from 'express';
import PostsRouter from './routes/posts.router.js';
        
const app = express();
const PORT = 3017;
        
app.use(express.json());
app.use('/api', [PostsRouter]);
        
app.listen(PORT, () => {
console.log(PORT, '포트로 서버가 열렸어요!');
});

Prisma DB, Table 생성하기

# 해당 프로젝트에 schema.prisma에 정의된 테이블을 MySQL에 생성합니다.
npx prisma db push
profile
황세민

0개의 댓글