해당 포스트는
ORM인prisma에 대한 개념과 기본적인 사용법을 정리한 게시글입니다.
📝 용어 정리
- ORM:- Object Relational Mapping으로- JavaScript의 객체를- DB의- table과 맵핑시켜주는 것을 의미
- schema.prisma: 코드상으로 모델 생성 및 관계 설정을 하는 파일
- draft migration file: 현재 작성된- schema.prisma를 기준으로 생성된- .sql
- _prisma_migrations:- prisma에서- migrate기록을 확인하기 위해- DB에 저장한- table
⚙️ 기본 세팅
mysql을 기준으로 작성했습니다.
0️⃣ 설치
1
npm i prisma @prisma/client
추가로 VSCode를 사용한다면 Prisma라는 Extension을 설치하면 schema.prisma에서 모델을 정의할 때 도움이 됩니다.
1️⃣ 초기화
1
npx prisma init
2️⃣ 기본 제공 파일 수정
1. schema.prisma 수정
1
2
3
4
5
6
7
8
9
generator client {
  provider = "prisma-client-js"
}
// "postgresql" -> "mysql"로 바꾸기
datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}
2. .env 수정
1
DATABASE_URL="mysql://<유저명>:<비밀번호>@localhost:3306/<DB이름>”
🗃️ 모델 ( 관계 설정 )
아래 예시에서 1번을 기준으로 @relation()에 대해 살펴보겠습니다.
 fields는 Post에서 참조할 User의 식별자의 이름을 정하는 것입니다.
 references는 Foreign Key로 사용할 것을 지정합니다.
 onUpdate/onDelete는 DB에서 사용하는 것과 같은 의미입니다. ( Cascade는 참조 대상이 사라지면 본인도 제거하라는 의미죠 )
 User의 idx컬럼을 Foreign Key로 사용하되 Post에서는 userIdx라고 부르겠다는 의미입니다.
0️⃣ 모델 정의
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
model User {
  idx Int @id @default(autoincrement())
  // "User"와 "Profile" ( 1 : 1 )
  profile Profile?
  // "User"와 "Post" ( 1 : N )
  posts Post[]
  // "User"와 "Job" ( N : M ) ( 중간 테이블을 안만들면 prisma에서 알아서 이름을 지정해서 생성 ( "_jobtouser"라는 테이블 생성 ) )
  jobs Job[]
  // "User"와 "User" ( 자기참조 N : M ) ( 중간 테이블을 명시적으로 만들었기 때문에 "follows"라는 테이블이 생성 )
  follower Follows[] @relation("follower")
  following Follows[] @relation("following")
}
// 1 : 1
model Profile {
  idx Int @id @default(autoincrement())
  name String @db.VarChar(30)
  email String @unique @db.VarChar(50)
  // "User"와 "Profile" ( 1 : 1 ) ( 1 : 1이기 때문에 User측에서 아래 내용을 작성해도 됩니다. )
  user User @relation(fields: [userIdx], references: [idx], onUpdate: Cascade, onDelete: Cascade)
  userIdx Int @unique
}
// 1 : N
model Post {
  idx Int @id @default(autoincrement())
  title String
  description String @db.VarChar(500)
  // "User"와 "Post" ( 1 : N ) ( >> 1번 << )
  user User @relation(fields: [userIdx], references: [idx], onUpdate: Cascade, onDelete: Cascade)
  userIdx Int
}
// N : M
model Job {
  idx Int @id @default(autoincrement())
  name String @db.VarChar(100)
  users User[]
}
// 자기참조 N : M ( 중간 테이블 )
model Follows {
  follower    User @relation("follower", fields: [followerIdx], references: [idx], onUpdate: Cascade, onDelete: Cascade)
  followerIdx  Int
  following   User @relation("following", fields: [followingIdx], references: [idx], onUpdate: Cascade, onDelete: Cascade)
  followingIdx Int
  @@id([followerIdx, followingIdx])
}
1️⃣ migrate로 생성된 SQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
-- CreateTable
CREATE TABLE `User` (
    `idx` INTEGER NOT NULL AUTO_INCREMENT,
    PRIMARY KEY (`idx`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Profile` (
    `idx` INTEGER NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(30) NOT NULL,
    `email` VARCHAR(50) NOT NULL,
    `userIdx` INTEGER NOT NULL,
    UNIQUE INDEX `Profile_email_key`(`email`),
    UNIQUE INDEX `Profile_userIdx_key`(`userIdx`),
    PRIMARY KEY (`idx`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Post` (
    `idx` INTEGER NOT NULL AUTO_INCREMENT,
    `title` VARCHAR(191) NOT NULL,
    `description` VARCHAR(500) NOT NULL,
    `userIdx` INTEGER NOT NULL,
    PRIMARY KEY (`idx`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Job` (
    `idx` INTEGER NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(100) NOT NULL,
    PRIMARY KEY (`idx`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Follows` (
    `followerIdx` INTEGER NOT NULL,
    `followingIdx` INTEGER NOT NULL,
    PRIMARY KEY (`followerIdx`, `followingIdx`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `_JobToUser` (
    `A` INTEGER NOT NULL,
    `B` INTEGER NOT NULL,
    UNIQUE INDEX `_JobToUser_AB_unique`(`A`, `B`),
    INDEX `_JobToUser_B_index`(`B`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Profile` ADD CONSTRAINT `Profile_userIdx_fkey` FOREIGN KEY (`userIdx`) REFERENCES `User`(`idx`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Post` ADD CONSTRAINT `Post_userIdx_fkey` FOREIGN KEY (`userIdx`) REFERENCES `User`(`idx`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Follows` ADD CONSTRAINT `Follows_followerIdx_fkey` FOREIGN KEY (`followerIdx`) REFERENCES `User`(`idx`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Follows` ADD CONSTRAINT `Follows_followingIdx_fkey` FOREIGN KEY (`followingIdx`) REFERENCES `User`(`idx`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_JobToUser` ADD CONSTRAINT `_JobToUser_A_fkey` FOREIGN KEY (`A`) REFERENCES `Job`(`idx`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `_JobToUser` ADD CONSTRAINT `_JobToUser_B_fkey` FOREIGN KEY (`B`) REFERENCES `User`(`idx`) ON DELETE CASCADE ON UPDATE CASCADE;
✍️ 마이그레이션
해당 내용은 Prisma migrate 블로그를 참고해서 요약한 내용이기 때문에 해당 블로그를 보시는 것을 추천합니다.
위 모델을 그대로 선언하고 명령어들을 순서대로 실행하면서 변화 과정을 테스트해보시면 이해에 도움이 됩니다.
 _prisma_migrations를 바탕으로 기존에 했던 migrate는 실행하지 않고 변경된 명령만 수행합니다.
0️⃣ migrate란
DB의 schema(table)를 변경할 수 있는 툴
1️⃣ 동작 흐름
- draft migration file생성
- draft migration file을- DB의- schema에 적용 및- _prisma_migrations에 추가
- generate artifacts적용 ( 코드상으로- Type이 만들어지고 적용되는 과정 )
2️⃣ 명령어
- npx prisma migrate dev --create-only:- draft migration file만 생성 ( 즉,- /prisma/migrations/생성시간_이름/migration.sql생성 )
- npx prisma migrate deploy:- DB에 적용 및- _prisma_migrations업데이트
- npx prisma generate:- prisma client생성 ( 즉, 타입을 만들고 코드에 적용 )
- npx prisma migrate dev: 위 3가지 명령어 순차적으로 실행 ( 즉,- .sql만들고- DB에 적용하고 코드에 적용 )
📌 코드로 사용하는 방법
이 부분은 사용할 수 있는 방법이 너무 많아서 생략하겠습니다.
 앞으로 사용해보면서 이런 내용은 추가할만하다라고 느껴지면 추가하겠습니다.
🌱 seed
기본 데이터들을 데이터 베이스에 넣는 방법입니다.
0️⃣ 세팅
1
2
# ts 코드를 실행해야 하기 때문에 설치합니다.
npm i ts-node
- package.json수정
1
2
3
4
5
6
{
  // ... 생략
  "prisma": {
    "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed/index.ts"
  },
}
1
2
3
4
5
# 아래 코드로 seed를 생성합니다.
npx prisma db seed
# 혹은 리셋시키면 자동으로 seed가 추가됩니다. ( 단, 이전에 넣었던 모든 데이터가 날아가고 seed만 남습니다. )
npx prisma migrate reset
1️⃣ seed 코드 작성 예시
아래 코드는 위의 예시의 Profile을 예로 seed를 만들어봤습니다.
 즉, 1번 유저의 프로필을 30개 만든 것입니다.
 아마 1번 유저가 없다면 오류가 나겠죠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
// type
import type { Prisma } from "@prisma/client";
// 가짜 profile 30개 반환하는 함수
const getDummyProfile = (): Prisma.ProfileCreateManyInput[] =>
  Array(30)
    .fill(null)
    .map((v, i) => ({
      name: "test" + i,
      email: "test" + i + "@naver.com",
      userIdx: 1,
    }));
async function main() {
  prisma.profile.createMany({
    skipDuplicates: true,
    data: getDummyProfile(),
  });
}
main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    // process.exit(1);
  });
😮 유용한 prisma 명령어들
- npx prisma studio:- DB를 쉽게 관리할 수 있도록 웹 브라우저로- UI제공 (- CRUD가능 )
- npx prisma migrate dev:- magration수행
- npx prisma migrate reset: 기존- DB의 데이터 모두 삭제하고 모든 마이그레이션 실행 후- seed실행
- npx prisma db pull: 데이터 베이스의 테이블을- prisma에 적용
- npx prisma db push:- magration없이- DB에 적용
😶 소소한 팁
0️⃣ 타입이 바로 적용 안되는 경우
npx prisma generate 혹은 npx prisma migrate dev를 실행한 후 타입이 적용이 안된다면 prisma 타입 정의된 파일에 들어갔다가 다시 나오면 됩니다.
1️⃣ 만들어진 타입 사용하기
model을 생성하고 적용했다면 그에 맞는 타입이 만들어집니다.
위의 예시의 Profile을 예로 들어보겠습니다.
- 생성된 타입 형태
1
2
3
4
5
6
7
// 이런 타입이 생성되었고, "export"기 때문에 그대로 가져다 사용하면 됩니다.
export type Profile = {
  idx: number
  name: string
  email: string
  userIdx: number
}
- 타입 수정 방법
1
2
3
4
5
6
// 혹시 타입을 쓰다가 타입을 조금씩 바꿔서 쓰고 싶다면 typescript utility를 사용하면 됩니다.
import type { Profile } from "@prisma/client";
// 만약 userIdx를 안 받는다면 아래와 같이 수정해서 사용하면 되겠죠
// 그러면 Proflie 자체를 수정하더라도 아래 타입을 그대로 사용하기만 하면 됩니다. 굳이 수정할 필요가 없어지죠
export type SimpleProfile = Omit<Profile, "userIdx">;
- express사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import express from "express";
// type
import type { Request, Response, NextFunction } from "express";
import type { Profile } from "@prisma/client";
router.get(
  "/",
  async (
    req: Request<{}, {}, {}, { name: string }>,
    res: Response<{ message: string; profile: Profile }>,
    next: NextFunction
  ) => {
    try {
      const { name } = req.query;
      const profile = await prisma.profile.findFirst({ name });
      return res.json({
        message: `"${name}"님의 프로필을 찾았습니다.`,
        profile
      });
    } catch (error) {
      next(error);
    }
  }
);
cursor 사용하기
prisma의pagination을 참고해주세요!
특정 데이터를 기준으로 데이터들을 가져오는 경우 cursor를 사용하면 쉽게 가져올 수 있습니다.
 ( 제 기준에서는 무한 스크롤링에 주로 사용합니다. )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 임의로 값 부여
const take = 10;
const lastIdx = 5; // 기본 값 "-1" ( 즉, 첫 패치 )
const posts = await prisma.post.findMany({
  where: {},
  // 가져올 개수
  take,
  // cursor 기준으로 무시할 개수 ( 기본 값이 아니면 첫 번째 값 무시 ( 중복 제거 ) )
  skip: lastIdx === -1 ? 0 : 1,
  // 첫 패치가 아니라면 커서 이동 ( 커서를 기준으로 시작 )
  ...(lastIdx !== -1 && { cursor: { idx: lastIdx } }),
  // 최신순 정렬
  orderBy: { createdAt: "desc" },
});