Using Prisma with a PlanetScale database

기운찬곰·2023년 7월 24일
1

Next.js 이모저모

목록 보기
8/8
post-thumbnail

Overview

Prisma Schema 작성 하는 방법은 공식 문서에서 잘 설명 되어있습니다.

그래서 이와 관련한 글을 작성할 지 고민했는데 그래도 마침 이번에 새로 알게 된 사실도 있고, PlanetScale database에서의 Relation mode 작성 방법과 Prisma Models, Relations 설정 위주로 글을 정리해보면 좋을 거 같다는 생각이 들었습니다.

마지막으로 간단한 Reddit 클론 Schema 설계 예시까지 살펴보고 마무리해보려고 합니다. 이후 비슷한 설계를 할 때 도움이 될 거 같습니다.


Relation mode

Introduction

공식 문서 : https://www.prisma.io/docs/concepts/components/prisma-schema/relations/relation-mode

Prisma에는 records 간의 관계가 어떻게 적용되는지를 명시하는 두 가지 관계 모드, 즉 foreignKeysprisma이 있습니다.

관계형 데이터베이스와 함께 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) 설정 - ON DELETE, ON UPDATE etc...

이러한 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 DELETEON UPDATE 참조 작업은 CASCADE 옵션을 지정합니다. 이 옵션은 사용자에 속하는 모든 게시물도 삭제하거나 업데이트합니다. (그니까 해당 사용자가 삭제되면 그 사용자가 쓴 게시글도 다 삭제된다는 뜻이고, 유저 정보가 업데이트 된다면 같이 업데이트 된다는 뜻이죠)

MongoDB 또는 PlanetScale과 같은 일부 데이터베이스는 외래 키를 지원하지 않습니다. 또한 개발자들은 일반적으로 외래 키를 지원하는 관계형 데이터베이스에서 외래 키를 사용하지 않는 것을 선호할 수 있습니다.

이러한 상황에서 Prisma은 관계형 데이터베이스에서 관계의 일부 속성을 에뮬레이트(?)하는 prisma relation mode를 제공합니다. prisma relation mode를 사용하도록 설정한 상태에서 Prisma Client를 사용하면 쿼리 동작이 동일하거나 유사하지만 참조 작업과 일부 제약 조건은 데이터베이스가 아닌 Prisma 엔진에서 처리됩니다. (오호~ 그렇군요 👍)

How to set the relation mode in your Prisma schema

관계 모드를 설정하려면 datasource 블록에 relationMode 필드를 추가합니다:

datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"   // ✅ 추가
}
  • foreignKeys : 외래 키를 사용하여 데이터베이스의 관계를 처리합니다. 이 옵션은 모든 관계형 데이터베이스 커넥터의 기본 옵션이며 datasource본 블록에서 relationMode가 명시적으로 설정되지 않은 경우 활성화됩니다.
  • prisma : 이것은 Prisma Client의 관계를 에뮬레이트합니다. 또한 MySQL 커넥터를 PlanetScale 데이터베이스와 함께 사용할 때도 이 옵션을 실행해야 합니다.

⭐️ PlanetScale를 사용할 때는 relationMode를 "prisma"로 설정해야 되는 군요. 사용 시 주의해야겠습니다.

PlanetScale - Operating without foreign key constraints

참고 : https://planetscale.com/docs/learn/operating-without-foreign-key-constraints

PlanetScale 공식 문서에도 foreign key constraints 없이 동작한다는 내용이 있길래 가져와봤습니다. 그렇다면 왜 지원하지 않을까요? 🤔

외래 키 제약 조건은 외래 키 관계의 무결성(참조 무결성)을 강제하는 구현인 데이터베이스 구성입니다. 즉, "parent" 테이블에 적절한 행이 있는 경우에만 "child" 테이블이 부모 테이블을 참조할 수 있습니다. 또한 제약 조건은 다른 방법으로 "연결되지 않은 행"의 존재를 방지합니다.

PlanetScale 은 FOREIGN KEY constraints을 지원하지 않습니다. 두 가지 주요 기술적 이유가 있습니다:

  1. FOREIGN KEY constraints이 MySQL(또는 InnoDB 스토리지 엔진)에서 구현되는 방식은 Online DDL 작업을 방해합니다. Vitess 블로그 게시물에서 자세히 알아보십시오.
  2. 단일 MySQL 서버 범위로 제한된 FOREIGN KEY constraints은 일단 데이터가 증가하고 여러 데이터베이스 서버로 분할되면 유지 관리가 불가능합니다. 이 문제는 일반적으로 기능적 파티셔닝/샤딩 및/또는 수평 샤딩을 도입할 때 발생합니다.

branching, 개발자 소유 스키마 변경 및 배포, non-blocking 스키마 변경 등과 같은 온라인 DDL의 이점과 무제한 확장 수단으로서의 샤딩의 이점이 FORIENT KEY 제약 조건의 이점을 능가한다고 생각합니다.

즉, 데이터베이스 수준이 아닌 애플리케이션 수준에서 참조 무결성을 적용하면 이러한 모든 이점을 누릴 수 있습니다.

저도 이전에 회사에서 DB ERD 만들어보라고 하셔서 학교에서 배운대로 정석으로 만들었더니(외래키 사용해서 테이블 다 쪼개서 만들었는데...), 실무에서는 이렇게 잘 안쓴다고 하셨던게 기억나네요. 아무튼 외래키 사용은 장단점이 있어서 상황에 맞게 사용하는게 좋을 거 같습니다.

다음은 외래키 사용과 관련해서 읽어보면 좋을 듯한 글들 입니다.


Data model

공식 문서 : https://www.prisma.io/docs/concepts/components/prisma-schema/data-model

Prisma 스키마의 데이터 모델 정의 부분은 애플리케이션 모델(Prisma 모델이라고도 함)을 정의합니다.

  • 애플리케이션 도메인의 entities를 나타냅니다.
  • 데이터베이스에서 tables(PostgreSQL와 같은 관계형 데이터베이스) 또는 collections(MongoDB)에 해당하는 부분입니다.
  • 생성된 Prisma Client API에서 사용할 수 있는 queries의 기초를 구성합니다.
  • TypeScript와 함께 사용할 때, Prisma Client는 모델에 대해 생성된 type definitions와 데이터베이스 액세스를 완전히 type safe 하게 하기 위한 모델의 variations을 제공합니다.

블로그 서비스를 만든다고 가정해보고 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
}

데이터 모델 정의는 다음과 같이 구성됩니다:

  • 모델 간의 relations 를 포함하여 여러 필드를 정의하는 Models
  • Enums (connector가 Enums를 지원하는 경우)
  • 필드 및 모델의 동작을 변경하는 Attributes 와 functions

✍️ Data Model 부분은 이 정도만 알고 넘어가면 될 거 같습니다. 나머지 필요한 부분은 그때 그때마다 찾아서 보면 되겠네요. 중요한 건 Relations 설정이니까요.


Relations

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)
}

  • Two relation fields : author 와 posts. 관계 필드는 Prisma 수준에서 모델 간의 연결을 정의하며 데이터베이스에 존재하지 않습니다. 이 필드는 Prisma Client를 생성하는 데 사용됩니다.
  • @relation 특성에 의해 참조되는 스칼라 authorId 필드. 이 필드는 데이터베이스에 존재합니다. Post과 User를 연결하는 외래 키입니다.

Prisma 수준에서 두 모델 사이의 연결은 항상 관계의 each side에 있는 relation field로 표시됩니다.

Types of relations (관계 유형)

Prisma의 관계에는 세 가지 유형이 있습니다:

  • One-to-one (also called 1-1 relations)
  • One-to-many (also called 1-n relations)
  • Many-to-many (also called m-n relations)

아래 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-relations

관계 필드는 자체 모델(자기 자신)을 참조할 수 있으며, 이 경우 관계를 self-relation라고 합니다. Self-relations는 1-1, 1-n, m-n 등 임의의 카디널리티를 가질 수 있습니다. Self-relations에는 항상 @relation 속성이 필요합니다.

self-relation 에 해당하는 예로는 상위/하위 카테고리, 댓글/답글 설정 등이 있죠. 특수한 경우에 사용됩니다.

one-to-one 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")
}
  • "사용자는 하나 또는 0의 predecessors(=전임자)를 가질 수 있습니다" (예를 들어, Sarah는 블로그 소유자로서 Mary의 predecessor 입니다)
  • "사용자는 하나 또는 0개의 successor(=후계자)를 가질 수 있습니다" (예를 들어, Mary는 Sarah의 블로그 소유자로서의 successor 입니다)

one-to-one self-relation 를 작성하려면:

  • 관계의 양쪽은 동일한 이름을 공유하는 @relation 특성(이 경우 BlogOwnerHistory)을 정의해야 합니다.
  • 하나의 관계 필드에는 fully annotated 되어야 합니다. 이 예에서 successor 필드는 fieldreferences 인수를 모두 정의합니다.
  • 하나의 관계 필드는 외래 키로 백업해야 합니다. success 필드는 succeusId 외래 키로 지원되며, 이 키는 id 필드의 값을 참조합니다. successorId 스칼라 관계 필드에는 일대일 관계를 보장하기 위해 @unique 속성이 필요합니다.

one-to-many self-relation

다음은 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")
}

이 관계는 다음을 나타냅니다.

  • "사용자는 0명 또는 한 명의 선생님을 갖습니다."
  • "사용자는 0명 또는 한 명 이상의 학생을 갖습니다."

Referential actions


(예시) Reddit Clone Schema 설계

참고 : 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 설계와 인덱스 설정, 그리고 성능과 튜닝에 있습니다. 아직 전문성이 없다고 해야 할까요. 뭐... 비록 백엔드 개발자는 아니지만요. 아무튼 관심은 많습니다.


참고 자료

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글