본문으로 건너뛰기
Advertisement

15.3 Prisma 관계 — 1:1, 1:N, M:N 타입 안전 쿼리

관계 유형

1:1 관계 (One-to-One)

model User {
id Int @id @default(autoincrement())
profile Profile?
}

model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @unique // @unique가 1:1을 표현
}
// 1:1 생성
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
profile: {
create: { bio: 'TypeScript 개발자' },
},
},
include: { profile: true },
})

// 프로필 업데이트
const updated = await prisma.user.update({
where: { id: 1 },
data: {
profile: {
upsert: {
create: { bio: '새 프로필' },
update: { bio: '수정된 프로필' },
},
},
},
include: { profile: true },
})

1:N 관계 (One-to-Many)

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
}
// 사용자와 게시글 함께 조회
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
select: {
id: true,
title: true,
createdAt: true,
},
},
},
})

// 게시글로 작성자 접근
const post = await prisma.post.findUnique({
where: { id: 1 },
include: { author: true },
})

// 중첩 필터 — 게시글이 있는 사용자만
const activeAuthors = await prisma.user.findMany({
where: {
posts: {
some: { published: true }, // 게시된 글이 하나라도 있는
},
},
})

// 게시글이 없는 사용자
const noPostUsers = await prisma.user.findMany({
where: {
posts: {
none: {}, // 글이 하나도 없는
},
},
})

// 모든 게시글이 게시된 사용자
const allPublishedAuthors = await prisma.user.findMany({
where: {
posts: {
every: { published: true },
},
},
})

M:N 관계 (Many-to-Many)

// 암묵적 M:N (중간 테이블 자동 생성)
model Post {
id Int @id @default(autoincrement())
tags Tag[]
}

model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
// 명시적 M:N (중간 테이블 직접 정의)
model Post {
id Int @id @default(autoincrement())
postTags PostTag[]
}

model Tag {
id Int @id @default(autoincrement())
postTags PostTag[]
}

model PostTag {
post Post @relation(fields: [postId], references: [id])
postId Int
tag Tag @relation(fields: [tagId], references: [id])
tagId Int
createdAt DateTime @default(now())

@@id([postId, tagId]) // 복합 기본키
}
// 암묵적 M:N — 태그 연결
const post = await prisma.post.update({
where: { id: 1 },
data: {
tags: {
connect: [{ id: 1 }, { id: 2 }], // 기존 태그 연결
create: [{ name: '새태그' }], // 새 태그 생성 및 연결
disconnect: [{ id: 3 }], // 연결 해제
set: [{ id: 1 }, { id: 2 }], // 기존 연결 모두 교체
},
},
include: { tags: true },
})

// 태그로 게시글 조회
const typescriptPosts = await prisma.post.findMany({
where: {
tags: {
some: { name: 'TypeScript' },
},
},
include: { tags: true },
})

// 명시적 M:N — 중간 테이블에 데이터 추가
const postTag = await prisma.postTag.create({
data: {
postId: 1,
tagId: 2,
},
})

자기 참조 관계 (Self-relation)

model Category {
id Int @id @default(autoincrement())
name String
parent Category? @relation("CategoryTree", fields: [parentId], references: [id])
parentId Int?
children Category[] @relation("CategoryTree")
}
// 카테고리 트리 조회
const rootCategories = await prisma.category.findMany({
where: { parentId: null },
include: {
children: {
include: {
children: true, // 2단계까지 로드
},
},
},
})

관계 필터링

// 중첩 select로 특정 필드만
const result = await prisma.user.findMany({
select: {
id: true,
name: true,
posts: {
select: {
title: true,
published: true,
},
where: { published: true },
},
_count: {
select: { posts: true }, // 게시글 수 포함
},
},
})
// result[0].posts → { title: string; published: boolean }[]
// result[0]._count.posts → number

고수 팁

Fluent API (체이닝 스타일)

// 특정 사용자의 게시글 조회 (중첩 관계 체이닝)
const posts = await prisma.user
.findUnique({ where: { id: 1 } })
.posts({ where: { published: true } })

// 게시글의 작성자 정보
const author = await prisma.post
.findUnique({ where: { id: 1 } })
.author()

// 사용자 프로필
const profile = await prisma.user
.findUnique({ where: { id: 1 } })
.profile()
Advertisement