Nest.js를 배워보자/15. NestJS & Prisma 완벽 가이드

🌐 4부. 관계형 데이터 쿼리 마스터하기

_Blue_Sky_ 2025. 12. 3. 19:43
728x90

Prisma의 진정한 강점은 관계형 데이터를 다루는 강력하고 직관적인 방식에 있습니다.
이 섹션에서는 일대다(One-to-Many), 다대다(Many-to-Many) 관계 설정부터, 데이터 로딩 최적화 및 복합적인 생성/수정(Nested Write) 기법까지 다룹니다.


4.1. 일대다 관계 (One-to-Many)

일대다 관계는 가장 흔한 관계이며, 한 모델이 다른 모델의 여러 인스턴스를 소유할 때 사용됩니다 (예: 한 User는 여러 Post를 작성).

🛠️ 스키마 정의

일대다 관계는 두 가지 필드로 정의됩니다.

  1. 외래 키 필드 (Foreign Key Field): N 쪽에 위치하며, 1 쪽 모델의 ID를 저장합니다. (예: Post 모델의 authorId)
  2. 관계 필드 (Relation Field):
    • N 쪽: 외래 키를 참조하는 @relation 데코레이터와 함께 정의됩니다.
    • 1 쪽: 관계된 모델의 배열 타입으로 정의됩니다.
// 1쪽 (User)
model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  posts Post[] // ⭐️ N쪽 모델의 배열 타입으로 정의 (관계 필드)
}

// N쪽 (Post)
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  authorId  Int      // ⭐️ 외래 키 필드
  author    User     @relation(fields: [authorId], references: [id]) // ⭐️ 관계 필드
}

💾 연결된 데이터 생성/조회

1. 관계를 이용한 데이터 생성

connect를 사용하여 기존 User와 Post를 연결하거나, create를 사용하여 연결과 동시에 새 User를 만들 수 있습니다.

// 🔗 기존 User에 Post 연결 생성
async createPost(title: string, authorId: number) {
  return this.prisma.post.create({
    data: {
      title: title,
      author: {
        connect: { id: authorId }, // ⭐️ 기존 User ID를 이용해 연결
      },
    },
  });
}

2. 관계 데이터 조회 (Eager Loading)

조회 시 include를 사용하여 연결된 데이터를 함께 가져옵니다. (4.3절에서 자세히 설명)

async getUserWithPosts(id: number) {
  return this.prisma.user.findUnique({
    where: { id },
    include: { posts: true }, // ⭐️ 연결된 모든 posts를 한 쿼리로 가져옴
  });
}

4.2. 다대다 관계 (Many-to-Many)

다대다 관계는 하나의 레코드가 다른 모델의 여러 레코드와 연결될 수 있고, 그 반대도 가능한 관계입니다 (예: 하나의 Post에는 여러 Tag가, 하나의 Tag는 여러 Post에 연결됨).

🛠️ 스키마 정의 (중간 테이블 자동 관리)

Prisma는 명시적인 중간 테이블(조인 테이블) 모델 없이도 다대다 관계를 자동으로 관리합니다.

// Post와 Tag는 서로의 배열 타입 필드를 가집니다.
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  tags      Tag[]    // ⭐️ N쪽 모델의 배열 타입 (관계 필드)
}

model Tag {
  id    Int     @id @default(autoincrement())
  name  String  @unique
  posts Post[] // ⭐️ N쪽 모델의 배열 타입 (관계 필드)

  // Prisma가 자동으로 _PostToTag라는 중간 테이블을 생성/관리합니다.
}

💾 다대다 관계 쿼리

1. 연결 및 연결 해제

connect, disconnect, set을 사용하여 다대다 관계를 설정합니다.

async addTagsToPost(postId: number, tagIds: number[]) {
  return this.prisma.post.update({
    where: { id: postId },
    data: {
      tags: {
        // ⭐️ 기존 태그는 그대로 두고, 새 태그 ID들만 연결
        connect: tagIds.map(id => ({ id })),
        // set: 모든 기존 연결을 끊고, 새 ID들로 대체
        // disconnect: 특정 ID들의 연결을 끊음
      },
    },
    include: { tags: true },
  });
}

4.3. 관계형 데이터 로딩: include와 select

관계형 데이터를 가져오는 방식은 성능에 큰 영향을 미칩니다. Prisma는 Eager Loading을 사용하여 N+1 문제를 방지합니다.

🚀 Eager Loading: include

include는 주 모델을 조회할 때 연결된 관계 데이터를 함께 가져오도록 지시합니다. Prisma는 내부적으로 조인(Join) 또는 배치(Batch) 쿼리를 사용하여 단 한 번의 쿼리로 모든 데이터를 가져옵니다.

async getPostsWithAuthorAndTags() {
  return this.prisma.post.findMany({
    include: {
      author: true, // ⭐️ User 객체 전체 포함
      tags: { // ⭐️ Tag 목록 포함 (선택적으로 포함 필터링 가능)
        select: {
          name: true,
        },
      },
    },
  });
}
// N+1 문제: 목록을 가져온 후, 목록의 각 항목에 대해 별도의 쿼리를 날려 관계 데이터를 가져오는 비효율적인 상황
// include를 사용하면 이를 방지할 수 있습니다.

✂️ 필드 선택: select

데이터 전송량(Payload)을 줄이고 성능을 최적화하기 위해, 필요한 필드만 선택적으로 가져와야 합니다.

  • select는 포함할 필드만 명시적으로 지정합니다.
  • select와 include는 같은 레벨에서 함께 사용할 수 없습니다. select를 사용하는 경우, 관계 데이터도 select 객체 내부에 정의해야 합니다.
async getPostTitlesWithAuthorName() {
  return this.prisma.post.findMany({
    select: {
      id: true,
      title: true,
      author: { // ⭐️ 관계 필드를 select 내에 정의
        select: {
          name: true, // User 모델에서 이름만 가져옴
        },
      },
    },
  });
}

4.4. 관계 데이터를 통한 생성/수정 (Nested Write)

중첩 쓰기(Nested Write)는 하나의 쿼리 요청으로 주 모델과 관계된 모델의 데이터를 동시에 생성, 수정, 또는 연결/연결 해제하는 강력한 기능입니다.

📝 중첩 생성 (create)

하나의 트랜잭션으로 User를 생성하면서 연결된 Post도 함께 생성할 수 있습니다.

async createCompleteUser(email: string, postTitle: string) {
  return this.prisma.user.create({
    data: {
      email: email,
      // ⭐️ posts 관계 필드에 중첩 쓰기 (Post 생성)
      posts: {
        create: {
          title: postTitle,
          content: 'Nested write example content',
        },
      },
    },
    include: { posts: true },
  });
}

📝 중첩 수정 (update)

update 쿼리 내에서 관계된 모델을 create, update, delete, connect, disconnect 등으로 조작할 수 있습니다.

async updateUserAndPosts(userId: number, newPostTitle: string, postIdToUpdate: number) {
  return this.prisma.user.update({
    where: { id: userId },
    data: {
      // 1. 새로운 Post 생성
      posts: {
        create: { title: newPostTitle },
        // 2. 기존 Post 수정
        update: {
          where: { id: postIdToUpdate },
          data: { content: 'Updated content via nested write' },
        },
        // 3. 특정 Post 삭제
        // delete: [{ id: postIdToDelete }],
      },
    },
    include: { posts: true },
  });
}
728x90