Prisma Schema 작성 하는 방법은 공식 문서에서 잘 설명 되어있습니다.
그래서 이와 관련한 글을 작성할 지 고민했는데 그래도 마침 이번에 새로 알게 된 사실도 있고, PlanetScale database에서의 Relation mode 작성 방법과 Prisma Models, Relations 설정 위주로 글을 정리해보면 좋을 거 같다는 생각이 들었습니다.
마지막으로 간단한 Reddit 클론 Schema 설계 예시까지 살펴보고 마무리해보려고 합니다. 이후 비슷한 설계를 할 때 도움이 될 거 같습니다.
공식 문서 : https://www.prisma.io/docs/concepts/components/prisma-schema/relations/relation-mode
Prisma에는 records 간의 관계가 어떻게 적용되는지를 명시하는 두 가지 관계 모드, 즉 foreignKeys
와 prisma
이 있습니다.
관계형 데이터베이스와 함께 Prisma를 사용하는 경우, 기본적으로 Prisma는 foreign keys를 이용한 데이터베이스 수준의 레코드 사이의 관계를 적용하는 foreignKeys relation mode
를 사용합니다. foreign keys는 다른 테이블의 primary key를 기반으로 값을 가지는 하나의 테이블의 column 또는 group of columns 입니다.
보통 User와 Post 사이의 관계를 표현할 때, 다음과 같이 표현할 수 있습니다. (DBdiagram.io 사용). 여기서 Post의 user_id가 외래키가 되는 것이죠.
foreign keys를 사용하면 다음을 수행할 수 있습니다:
이러한 constraints과 referential actions은 데이터의 참조 무결성(referential integrity) 을 보장합니다.
-- AddForeignKey
ALTER TABLE "Post"
ADD CONSTRAINT "Post_authorId_fkey"
FOREIGN KEY ("authorId")
REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
위 경우 Post 테이블의 authorId 열에 있는 외부 키 제약 조건은 User 테이블의 id 열을 참조하고 게시물에 존재하는 작성자가 있어야 함을 보장합니다. 사용자를 업데이트하거나 삭제하는 경우, ON DELETE
및 ON UPDATE
참조 작업은 CASCADE
옵션을 지정합니다. 이 옵션은 사용자에 속하는 모든 게시물도 삭제하거나 업데이트합니다. (그니까 해당 사용자가 삭제되면 그 사용자가 쓴 게시글도 다 삭제된다는 뜻이고, 유저 정보가 업데이트 된다면 같이 업데이트 된다는 뜻이죠)
MongoDB 또는 PlanetScale과 같은 일부 데이터베이스는 외래 키를 지원하지 않습니다. 또한 개발자들은 일반적으로 외래 키를 지원하는 관계형 데이터베이스에서 외래 키를 사용하지 않는 것을 선호할 수 있습니다.
이러한 상황에서 Prisma은 관계형 데이터베이스에서 관계의 일부 속성을 에뮬레이트(?)하는 prisma relation mode
를 제공합니다. prisma relation mode를 사용하도록 설정한 상태에서 Prisma Client를 사용하면 쿼리 동작이 동일하거나 유사하지만 참조 작업과 일부 제약 조건은 데이터베이스가 아닌 Prisma 엔진에서 처리됩니다. (오호~ 그렇군요 👍)
관계 모드를 설정하려면 datasource 블록에 relationMode 필드를 추가합니다:
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
relationMode = "prisma" // ✅ 추가
}
⭐️ PlanetScale를 사용할 때는 relationMode를 "prisma"로 설정해야 되는 군요. 사용 시 주의해야겠습니다.
참고 : https://planetscale.com/docs/learn/operating-without-foreign-key-constraints
PlanetScale 공식 문서에도 foreign key constraints 없이 동작한다는 내용이 있길래 가져와봤습니다. 그렇다면 왜 지원하지 않을까요? 🤔
외래 키 제약 조건은 외래 키 관계의 무결성(참조 무결성)을 강제하는 구현인 데이터베이스 구성입니다. 즉, "parent" 테이블에 적절한 행이 있는 경우에만 "child" 테이블이 부모 테이블을 참조할 수 있습니다. 또한 제약 조건은 다른 방법으로 "연결되지 않은 행"의 존재를 방지합니다.
PlanetScale 은 FOREIGN KEY constraints을 지원하지 않습니다. 두 가지 주요 기술적 이유가 있습니다:
branching, 개발자 소유 스키마 변경 및 배포, non-blocking 스키마 변경 등과 같은 온라인 DDL의 이점과 무제한 확장 수단으로서의 샤딩의 이점이 FORIENT KEY 제약 조건의 이점을 능가한다고 생각합니다.
즉, 데이터베이스 수준이 아닌 애플리케이션 수준에서 참조 무결성을 적용하면 이러한 모든 이점을 누릴 수 있습니다.
저도 이전에 회사에서 DB ERD 만들어보라고 하셔서 학교에서 배운대로 정석으로 만들었더니(외래키 사용해서 테이블 다 쪼개서 만들었는데...), 실무에서는 이렇게 잘 안쓴다고 하셨던게 기억나네요. 아무튼 외래키 사용은 장단점이 있어서 상황에 맞게 사용하는게 좋을 거 같습니다.
다음은 외래키 사용과 관련해서 읽어보면 좋을 듯한 글들 입니다.
공식 문서 : https://www.prisma.io/docs/concepts/components/prisma-schema/data-model
Prisma 스키마의 데이터 모델 정의 부분은 애플리케이션 모델(Prisma 모델이라고도 함)을 정의합니다.
블로그 서비스를 만든다고 가정해보고 ERD를 작성해본 예시입니다.
이에 따라 Prisma Schem를 작성하면 다음과 같습니다. (직관적이라서 어떤 의미인지 알기 쉽습니다)
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}
enum Role {
USER
ADMIN
}
데이터 모델 정의는 다음과 같이 구성됩니다:
✍️ Data Model 부분은 이 정도만 알고 넘어가면 될 거 같습니다. 나머지 필요한 부분은 그때 그때마다 찾아서 보면 되겠네요. 중요한 건 Relations 설정이니까요.
relation는 Prisma schema에서 두 모델 사이의 연결입니다. 예를 들어, 한 사용자가 많은 블로그 게시물을 가질 수 있기 때문에 User와 Post 사이에는 일대다(one-to-many) 관계가 있습니다.
다음 Prisma schema는 User 모델과 Post 모델 간의 일대다 관계를 정의합니다.
model User {
id Int @id @default(autoincrement())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int // relation scalar field (used in the `@relation` attribute above)
}
@relation
특성에 의해 참조되는 스칼라 authorId
필드. 이 필드는 데이터베이스에 존재합니다. Post과 User를 연결하는 외래 키입니다.Prisma 수준에서 두 모델 사이의 연결은 항상 관계의 each side에 있는 relation field로 표시됩니다.
Prisma의 관계에는 세 가지 유형이 있습니다:
아래 ERD를 보면 User와 Profile은 일대일, User와 Post는 일대다, Post와 Category는 다대다 관계를 보여주네요.
model User {
id Int @id @default(autoincrement())
posts Post[]
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int @unique // relation scalar field (used in the `@relation` attribute above)
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int // relation scalar field (used in the `@relation` attribute above)
categories Category[]
}
model Category {
id Int @id @default(autoincrement())
posts Post[]
}
이 예에서는 암묵적인 many-to-many relations를 사용합니다. 이러한 관계는 관계를 명확하게 구분할 필요가 없는 한, @relation 속성이 필요하지 않습니다. (그렇군요. 예전에 typeorm 했을때도 이런 개념이 있었던거 같네요. 즉, 별도의 다대다 테이블을 자동으로 생성한다는 의미입니다.)
관계 필드는 자체 모델(자기 자신)을 참조할 수 있으며, 이 경우 관계를 self-relation라고 합니다. Self-relations는 1-1, 1-n, m-n 등 임의의 카디널리티를 가질 수 있습니다. Self-relations에는 항상 @relation
속성이 필요합니다.
self-relation 에 해당하는 예로는 상위/하위 카테고리, 댓글/답글 설정 등이 있죠. 특수한 경우에 사용됩니다.
다음은 one-to-one self-relation 예시 입니다.
model User {
id Int @id @default(autoincrement())
name String?
successorId Int? @unique
successor User? @relation("BlogOwnerHistory", fields: [successorId], references: [id])
predecessor User? @relation("BlogOwnerHistory")
}
one-to-one self-relation 를 작성하려면:
@relation
특성(이 경우 BlogOwnerHistory)을 정의해야 합니다.field
및 references
인수를 모두 정의합니다.@unique
속성이 필요합니다.다음은 one-to-many self-relation 예시 입니다.
model User {
id Int @id @default(autoincrement())
name String?
teacherId Int?
teacher User? @relation("TeacherStudents", fields: [teacherId], references: [id])
students User[] @relation("TeacherStudents")
}
이 관계는 다음을 나타냅니다.
참고 : https://github.com/joschan21/breadit/blob/master/prisma/schema.prisma
여기 좋은 Reddit Clone Schema 설계 예시가 있습니다. 참고해보면 좋을 거 같습니다. 재밌네요.
model User {
id String @id @default(cuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
createdSubreddits Subreddit[] @relation("CreatedBy")
votes Vote[]
posts Post[]
comments Comment[]
commentVotes CommentVote[]
subscriptions Subscription[]
}
model Subreddit {
id String @id @default(cuid())
name String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
Creator User? @relation("CreatedBy", fields: [creatorId], references: [id])
creatorId String?
posts Post[]
subscribers Subscription[]
@@index([name])
}
model Subscription {
user User @relation(fields: [userId], references: [id])
userId String
subreddit Subreddit @relation(fields: [subredditId], references: [id])
subredditId String
@@id([userId, subredditId])
}
model Post {
id String @id @default(cuid())
title String
content Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
subreddit Subreddit @relation(fields: [subredditId], references: [id])
subredditId String
author User @relation(fields: [authorId], references: [id])
authorId String
comments Comment[]
votes Vote[]
}
model Comment {
id String @id @default(cuid())
text String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User @relation(fields: [authorId], references: [id])
authorId String
post Post @relation(fields: [postId], references: [id])
postId String
replyToId String?
replyTo Comment? @relation("ReplyTo", fields: [replyToId], references: [id], onDelete: NoAction, onUpdate: NoAction)
replies Comment[] @relation("ReplyTo")
votes CommentVote[]
}
enum VoteType {
UP
DOWN
}
model Vote {
user User @relation(fields: [userId], references: [id])
userId String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId String
type VoteType
@@id([userId, postId])
}
model CommentVote {
user User @relation(fields: [userId], references: [id])
userId String
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
commentId String
type VoteType
@@id([userId, commentId])
}
DBdiagram을 사용해서 ERD를 그려봤는데... 선 조작(정리)는 어떻게 안되나요...? ㅠㅠ
Prisma와 PlanetScale 공식문서를 읽어보면서 Database 공부도 하고 좋네요. 재미있는 시간이었습니다. 옛날 생각도 많이 납니다. 😂
저는 Database 실력이 부족하다고 생각하는데 그 이유는 DB 설계와 인덱스 설정, 그리고 성능과 튜닝에 있습니다. 아직 전문성이 없다고 해야 할까요. 뭐... 비록 백엔드 개발자는 아니지만요. 아무튼 관심은 많습니다.