Prisma Fluent API란?
Prisma Client의 Fluent API는 관계를 체이닝으로 탐색하는 쿼리 패턴입니다. include/select와 달리, 관계 체인의 끝에서 단일 관계 결과만 반환하여 타입 안전성과 쿼리 효율을 동시에 달성합니다. Prisma 5.x의 relationLoadStrategy와 결합하면 N+1 문제 없이 복잡한 관계 쿼리를 최적화할 수 있습니다.
1. Fluent API vs include/select
| 방식 | 반환 타입 | SQL 패턴 | 적합한 상황 |
|---|---|---|---|
include |
부모 + 자식 전체 | JOIN 또는 추가 SELECT | 부모와 자식을 함께 사용 |
select |
선택한 필드만 | 필드 제한 SELECT | 특정 컬럼만 필요 |
| Fluent API | 체인 끝의 관계만 | 단일 쿼리 | 관계 탐색 후 끝 노드만 필요 |
// Fluent API: 사용자의 주문의 상품만 가져오기
const products = await prisma.user
.findUnique({ where: { id: userId } })
.orders() // User → Order[] 관계 탐색
// Fluent는 단일 관계 체인에서만 동작
// .orders()는 중간 단계이므로 여기서 끊어야 함
// 실제 사용 패턴: 중간 관계를 건너뛰고 끝 노드 접근
const profile = await prisma.user
.findUnique({ where: { id: userId } })
.profile(); // User → Profile (1:1 관계)
// 반환: Profile | null (User 객체 없이 Profile만!)
// include 방식 비교
const userWithProfile = await prisma.user.findUnique({
where: { id: userId },
include: { profile: true },
});
// 반환: User & { profile: Profile | null }
2. 관계 필터: where 조건 깊이 활용
Prisma의 관계 필터는 관련 레코드의 존재/조건을 부모 쿼리의 WHERE에 적용합니다.
// some: 하나라도 조건을 만족하는 관계가 있으면
const usersWithExpensiveOrders = await prisma.user.findMany({
where: {
orders: {
some: {
total: { gte: 100000 },
status: 'COMPLETED',
},
},
},
});
// SQL: WHERE EXISTS (SELECT 1 FROM orders WHERE ...)
// every: 모든 관계가 조건을 만족해야
const loyalUsers = await prisma.user.findMany({
where: {
orders: {
every: {
status: { in: ['COMPLETED', 'SHIPPED'] },
},
},
},
});
// none: 조건을 만족하는 관계가 하나도 없어야
const noRefundUsers = await prisma.user.findMany({
where: {
orders: {
none: {
status: 'REFUNDED',
},
},
},
});
// SQL: WHERE NOT EXISTS (SELECT 1 FROM orders WHERE status = 'REFUNDED' ...)
3. 중첩 관계 필터: 3단계 이상
// 특정 카테고리 상품을 주문한 사용자 찾기
// User → Order → OrderItem → Product → Category
const users = await prisma.user.findMany({
where: {
orders: {
some: {
items: {
some: {
product: {
category: {
name: 'Electronics',
},
},
},
},
},
},
},
select: {
id: true,
email: true,
_count: {
select: { orders: true },
},
},
});
// 복합 조건: AND/OR/NOT 조합
const targetUsers = await prisma.user.findMany({
where: {
AND: [
// 조건 1: 최근 30일 내 주문 있음
{
orders: {
some: {
createdAt: { gte: thirtyDaysAgo },
},
},
},
// 조건 2: 환불 이력 없음
{
orders: {
none: {
status: 'REFUNDED',
},
},
},
// 조건 3: VIP 등급이거나 총 주문 5건 이상
{
OR: [
{ tier: 'VIP' },
{ orders: { some: { /* count 대신 존재 체크 */ } } },
],
},
],
},
});
4. relationLoadStrategy: JOIN vs 분리 쿼리
Prisma 5.x에서 도입된 relationLoadStrategy는 관계 로딩 방식을 제어합니다.
| 전략 | 동작 | 장점 | 단점 |
|---|---|---|---|
query (기본) |
부모 SELECT 후 자식 별도 SELECT | 큰 결과셋에서 안정적 | 쿼리 수 증가 |
join |
LATERAL JOIN으로 한 번에 | 단일 쿼리, 네트워크 왕복 최소 | 큰 관계에서 메모리 증가 |
// schema.prisma에서 preview feature 활성화
generator client {
provider = "prisma-client-js"
previewFeatures = ["relationJoins"]
}
// 쿼리별 전략 선택
const ordersWithItems = await prisma.order.findMany({
relationLoadStrategy: 'join', // LATERAL JOIN 사용
where: { status: 'PENDING' },
include: {
items: {
include: {
product: true,
},
},
user: {
select: { id: true, email: true },
},
},
});
// join 전략이 유리한 경우:
// - 관계 데이터가 적은 경우 (1:1, 소수의 1:N)
// - 네트워크 지연이 큰 경우 (외부 DB)
// - 여러 관계를 동시에 로드할 때
// query 전략이 유리한 경우:
// - 1:N 관계의 N이 매우 큰 경우
// - 깊은 중첩 관계 (3단계+)
// - 페이지네이션과 함께 사용할 때
5. _count와 집계 관계 쿼리
// 관계 카운트: 별도 쿼리 없이 COUNT 서브쿼리
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
_count: {
select: {
orders: true,
reviews: true,
followers: true,
},
},
},
orderBy: {
orders: { _count: 'desc' }, // 주문 많은 순 정렬
},
take: 10,
});
// 타입: { id, name, _count: { orders: number, reviews: number, followers: number } }
// 조건부 카운트: 필터링된 관계 수
const usersWithFilteredCounts = await prisma.user.findMany({
select: {
id: true,
_count: {
select: {
orders: {
where: { status: 'COMPLETED' }, // 완료된 주문만 카운트
},
},
},
},
});
6. 관계 쿼리 + Cursor 페이지네이션
// 관계 내부에서 커서 페이지네이션
async function getOrderItems(orderId: string, cursor?: string) {
const order = await prisma.order.findUnique({
where: { id: orderId },
select: {
id: true,
items: {
take: 20,
...(cursor && {
skip: 1,
cursor: { id: cursor },
}),
orderBy: { createdAt: 'desc' },
select: {
id: true,
quantity: true,
price: true,
product: {
select: { id: true, name: true, imageUrl: true },
},
},
},
},
});
const items = order?.items ?? [];
const nextCursor = items.length === 20
? items[items.length - 1].id
: null;
return { items, nextCursor };
}
// 부모 목록 + 관계 제한 (최근 3개 주문만)
const usersWithRecentOrders = await prisma.user.findMany({
take: 10,
select: {
id: true,
name: true,
orders: {
take: 3,
orderBy: { createdAt: 'desc' },
select: {
id: true,
total: true,
status: true,
},
},
},
});
7. 성능 패턴: 관계 쿼리 최적화
| 안티패턴 | 문제 | 최적화 |
|---|---|---|
| 루프 내 findUnique | N+1 쿼리 | findMany + include 한 번에 |
| 불필요한 include: true | 전체 컬럼 로드 | select로 필드 제한 |
| 깊은 중첩 include (4단계+) | 쿼리 폭발 | 2단계까지만, 나머지 별도 쿼리 |
| 관계 count에 findMany 사용 | 불필요한 데이터 전송 | _count 서브쿼리 사용 |
| 필터 없는 대량 관계 로드 | 메모리 초과 | take/skip으로 제한 |
// ❌ 안티패턴: 루프 내 개별 쿼리
const users = await prisma.user.findMany();
for (const user of users) {
const orders = await prisma.order.findMany({
where: { userId: user.id },
}); // N번 쿼리 발생!
}
// ✅ 최적화: 한 번에 로드
const usersWithOrders = await prisma.user.findMany({
include: {
orders: {
where: { status: 'ACTIVE' },
take: 5,
orderBy: { createdAt: 'desc' },
},
},
});
// ✅ 대안: 관계 ID만 먼저 수집 후 일괄 조회
const userIds = users.map(u => u.id);
const allOrders = await prisma.order.findMany({
where: { userId: { in: userIds } },
});
// 그룹핑은 애플리케이션에서 처리
const ordersByUser = Map.groupBy(allOrders, o => o.userId);
8. Prisma.$queryRaw 폴백
Prisma Client로 표현 불가능한 복잡한 관계 쿼리는 Raw SQL로 폴백합니다.
// 윈도우 함수 + 관계: 카테고리별 상위 3개 상품
const topProducts = await prisma.$queryRaw<TopProduct[]>`
WITH ranked AS (
SELECT
p.id,
p.name,
p.price,
c.name as category_name,
ROW_NUMBER() OVER (
PARTITION BY p.category_id
ORDER BY p.sales_count DESC
) as rank
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.status = 'ACTIVE'
)
SELECT id, name, price, category_name
FROM ranked
WHERE rank <= 3
ORDER BY category_name, rank
`;
// 타입 안전성 유지
interface TopProduct {
id: string;
name: string;
price: number;
category_name: string;
}
마무리
Prisma의 관계 쿼리는 include/select의 기본을 넘어, Fluent API 체이닝, some/every/none 관계 필터, relationLoadStrategy, _count 서브쿼리로 복잡한 데이터 접근 패턴을 타입 안전하게 표현할 수 있습니다. Prisma 관계 쿼리 기초를 이해한 뒤, Prisma Raw SQL 심화로 Client의 한계를 넘어서는 것을 권장합니다.