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 속성이 변경되었을 때 빠르게 수정이 가능하다"는 것이다.
# 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 your_server_file.js #첫 번째 방법
그 다음으로는 package.json에 nodemon을 이용하여 서버를 실행하는 스크립트(scripts)를 등록하는 방법이 있다.
// package.json
"scripts": {
"dev": "nodemon app.js" //스크립트에 등록
},
#터미널 실행 명령어
nodemon dev
npx prisma init 명령어를 입력하면 다음과 같은 폴더 구조를 생성한다.
내 프로젝트 폴더 이름
├── prisma
│ └── schema.prisma
├── .env
├── .gitignore
├── package.json
└── yarn.lock
Prisma는 연결하려는 데이터베이스의 속성을 schema.prisma 파일에서 관리하고 있다. 맨 처음에 schema.prisma
파일을 확인하면 아래의 2가지 구문이 작성되어 있는 것을 확인할 수 있다.
generator
구문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 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()
을 작성하지 않으면, 테이블명의 대문자는 전부 소문자로 치환된다.아이디와 비밀번호를 숨기기 위해 DB URL은 .env
파일에 숨겨지게 되는데 DB URL의 형식은 다음과 같다.
# .env
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
데이터베이스 URL의 구조는 다음과 같이 나눌 수 있다.
<Id>:<Password>@<RDS Endpoint>:<Port>
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>
// 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")
}
사용자(Users
) 모델은 사용자 정보(UserInfos
) 모델과 1:1 관계를 가지고 있습니다. 1:1 관계란 한 사용자가 하나의 사용자 정보만 가질 수 있고, 한 사용자 정보는 한 사용자에게만 속할 수 있다는 것을 의미합니다. 1:1 관계를 설정할 때는 다음과 같은 내용을 포함해야 합니다.
UserInfos
)에서 어떤 모델과 관계를 맺을지(Users
) 설정해야합니다.Users
)에서 어떤 모델이 관계를 맺는지(UserInfos
) 설정해야합니다.Users
)에서, 타입을 지정할 때 Optional Parameter(?
)를 지정해줘야합니다. 사용자는 사용자 정보가 존재하지 않을 수 있기 때문이죠. Users
: 참조할 모델을 지정합니다. 사용자(Users) 모델을 참조하므로 Users
로 작성되어있습니다.fields
: 사용자 정보(UserInfos) 모델에서 사용할 '외래 키(Foreign Key)' 컬럼을 지정합니다. 여기선, userId
컬럼으로 외래 키를 지정하였습니다.references
: 참조하는 다른 모델의 컬럼(Column)를 지정합니다. 여기선, 사용자(Users) 모델의 userId
컬럼을 참조합니다.onDelete
| onUpdate
: 참조하는 모델이 삭제 or 수정될 경우 어떤 행위를 할 지 설정합니다. Cascade
옵션을 선택하여 사용자가 삭제될 경우 그에 연결된 사용자 정보도 함께 삭제되도록 설정하였습니다.1:N 관계란 한 사용자는 여러개의 게시글을 작성할 수 있다는 것을 의미합니다. 여기서 사용자(Users
) 모델과 게시글(Posts
) 모델은 1:N 관계를 가지고 있습니다. 이런 1:N 관계를 설정할 때는 다음과 같은 내용을 포함해야 합니다.
Posts
)에서 어떤 모델과 관계를 맺을지(Users
) 설정해야합니다.Users
)에서 어떤 모델이 관계를 맺는지(Posts
) 설정해야합니다.Users
)에서, 타입을 지정할 때, 배열 연산자([]
)를 작성해줘야합니다. 사용자는, 여러개의 게시글을 가질 수 있기 때문이죠.현재 게시글 모델의 경우 작성한 사용자가 회원 탈퇴(onDelete
)하게 될 경우 작성한 모든 게시글이 삭제되도록 구현되어 있습니다. 이런 설정은 @relation
어노테이션을 사용하여 지정합니다.
// Users 테이블과 관계를 설정합니다.
user Users @relation(fields: [userId], references: [userId], onDelete: Cascade)
여기서 User
는 게시글(Posts
)이 참조하는 다른 모델을 지정하고, fields
는 게시글(Posts
) 모델에서 사용할 외래키 컬럼을 지정합니다. references
는 참조하는 다른 모델의 컬럼을 지정하고, onDelete
는 잠조하는 모델이 삭제될 경우 어떤 행위를 할 지 설정합니다. onDelete
의 경우, Cascade
옵션으로 사용자가 삭제될 경우 연관된 게시글 또한 삭제되도록 설정하였습니다.
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, '포트로 서버가 열렸어요!');
});
# 해당 프로젝트에 schema.prisma에 정의된 테이블을 MySQL에 생성합니다.
npx prisma db push