처음 Prisma를 사용할 때는 단순히 model만 작성했지만 실제 서비스 구조를 만들면서 데이터베이스 설계와 Prisma 문법을 함께 이해하는 것이 중요하다는 것을 느꼈다.
이번 글에서는 Course 모델을 예시로 Prisma Model 문법을 정리해보려고 한다.
전체 Course 모델 구조
model Course {
id String @id @default(uuid())
slug String @unique
title String
shortDescription String? @map("short_description")
description String? @map("description")
thumbnailUrl String? @map("thumbnail_url")
price Int @default(0)
discountPrice Int? @map("discount_price")
level String @default("BEGINNER")
instructorId String @map("instructor_id")
isPublished Boolean @default(false) @map("is_published")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
sections Section[]
lectures Lecture[]
categories CourseCategory[]
instructor User @relation(fields: [instructorId], references: [id])
enrollments CourseEnrollment[]
reviews CourseReview[]
questions Coursequestion[]
@@map("courses")
}
이 모델은 온라인 강의 플랫폼의 Course 테이블이다.
구조를 보면
Course
├ Section
├ Lecture
├ Category
├ Enrollment
├ Review
├ Question
이렇게 연결되어 있다.
Prisma에서 model은 데이터베이스 테이블을 의미한다.
model Course {
id String @id @default(uuid())
title String
}
이 코드는 실제 DB에서
courses
id
title
이런 테이블을 생성한다.
즉, Prisma model = Database Table 이라고 보면 된다.
@id (Primary Key)
id String @id @default(uuid())
의미는 기본키 설정이다.
대부분 서비스에서는 auto increment보다 uuid를 많이 사용한다.이유는 보안, 분산 시스템충돌 방지 때문이다.
shortDescription String? @map("short_description")
Prisma에서는shortDescription DB에서는 short_description로 저장된다.
왜 사용하는가?
백엔드 코드에서는 camelCase DB에서는 snake_case를 사용하기 때문이다.
실무에서도 많이 사용하는 방식이다.
@@map("courses") Prisma에서는 Course DB에서는 courses 테이블이 된다.
Prisma에서 가장 핵심은 relation 구조이다.
1:N 관계
// section model
courseId String
course Course @relation(fields: [courseId], references: [id])
위는 section model이다. 즉 section table
course와의 관계는 course 하나에 section은 N개가 될 수 있다. 위에 section model에 course Course @relation(fields: [courseId], references: [id]) 가 있다. course와 연결된다. fileds 는 현재 모델의 FK references는 참조 모델의 PK 즉 section의 courseId와 course의 id로 비교된다.
lecture Lecture @relation(fields: [lectureId], references: [id], onDelete: Cascade)
위와 같은 식으로 된다. lecture와 관계가 형성되어있고 관계가 맺어진 lecture 가 사라지면 현재 model도 삭제되는것.
CourseCategory
categories CourseCategory[]
courses Course[]
의미
Course 여러개
Category 여러개
Prisma가 자동으로 관리한다.
@@unique([userId, courseId])
예시
model LectureActivity {
id String @id @default(uuid())
userId String @map("user_id")
lectureId String @map("lecture_id")
progress Int @default(0)
isCompleted Boolean @default(false) @map("is_completed")
lastWatchedAt DateTime? @map("last_watched_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lecture Lecture @relation(fields: [lectureId], references: [id], onDelete: Cascade)
@@unique([userId, lectureId])
@@map("lecture_activities")
}
유저는 LectureActivity를 하나만 가질 수 있다.
videoStorageInfo Json?
동적 데이터 저장
예시
{
"url": "s3",
"duration": 300,
"quality": "1080p"
}
@@index는 데이터 조회 속도를 빠르게 만들기 위해 사용하는 Prisma 설정이다.
model Section {
id String @id @default(uuid())
title String
courseId String
course Course @relation(fields: [courseId], references: [id])
@@index([courseId])
}
이 설정은 courseId 기준으로 데이터를 빠르게 찾을 수 있도록 인덱스를 생성한다.
데이터베이스는 기본적으로 데이터를 조회할 때 전체 데이터를 순차적으로 탐색한다.
prisma.section.findMany({
where: {
courseId: "react-course"
}
})
실제 SQL
SELECT * FROM sections
WHERE course_id = 'react-course';
index가 없으면
모든 section 탐색
→ courseId 확인
→ 일치하는 데이터 반환
즉, 데이터가 많아질수록 조회 속도가 느려진다. 모든 section을 탐색하기 때문. 이걸 Full Table Scan이라고 한다.
index가 있으면
@@index([courseId])
DB는 내부적으로 courseId 기준 정렬된 탐색 구조를 만든다.이렇게 되면 이제 조회할때 courseId 위치 바로 찾기
가 가능하다. 전체 탐색이 아니라 빠르게 위치를 찾는다.
관계가 있는 컬럼은 대부분 조회에 사용된다.
prisma.courseReview.findMany({
where: {
courseId
},
orderBy: {
createdAt: "desc"
}
})
위와 같은 경우는 @@index([courseId, createdAt]) 를 설정하는 것이 좋다.
Prisma Model을 잘 설계하면 데이터 구조가 명확해지고
관계가 정리되고 서비스 확장이 쉬워진다.
단순히 model을 작성하는 것이 아니라 데이터베이스 설계를 한다는 생각으로 접근하는 것이 중요하다.