15.4 Prisma 고급 쿼리 — Raw SQL, 트랜잭션, 확장
Raw SQL
import { prisma } from './lib/prisma'
import { Prisma } from '@prisma/client'
// 태그 쿼리 (SQL 인젝션 방지 — $queryRaw 사용)
const email = 'alice@example.com'
const users = await prisma.$queryRaw<Array<{ id: number; name: string }>>`
SELECT id, name FROM "User" WHERE email = ${email}
`
// 동적 파라미터 — Prisma.sql 태그 사용
const minAge = 18
const maxAge = 65
const result = await prisma.$queryRaw<Array<{ count: bigint }>>`
SELECT COUNT(*) as count
FROM "User"
WHERE age BETWEEN ${minAge} AND ${maxAge}
`
// BigInt를 Number로 변환
const count = Number(result[0].count)
// 변경 쿼리 — $executeRaw
const affected = await prisma.$executeRaw`
UPDATE "Post" SET "viewCount" = "viewCount" + 1
WHERE id = ${1}
`
console.log(affected) // 영향 받은 행 수
트랜잭션
순차 트랜잭션
// 여러 작업을 하나의 트랜잭션으로 처리
const [user, post] = await prisma.$transaction([
prisma.user.create({
data: { email: 'alice@example.com', name: 'Alice' },
}),
prisma.post.create({
data: { title: '첫 글', authorId: 1 },
}),
])
인터랙티브 트랜잭션
// 트랜잭션 내에서 조건부 로직 처리
const result = await prisma.$transaction(async (tx) => {
// 출금
const sender = await tx.account.update({
where: { id: 'sender-id' },
data: { balance: { decrement: 100 } },
})
// 잔액 확인
if (sender.balance < 0) {
throw new Error('잔액 부족') // 자동 롤백
}
// 입금
const receiver = await tx.account.update({
where: { id: 'receiver-id' },
data: { balance: { increment: 100 } },
})
// 이체 기록
await tx.transfer.create({
data: {
senderId: 'sender-id',
receiverId: 'receiver-id',
amount: 100,
},
})
return { sender, receiver }
})
// 트랜잭션 옵션 설정
const result2 = await prisma.$transaction(
async (tx) => {
// ...
},
{
maxWait: 5000, // 최대 대기 시간 (ms)
timeout: 10000, // 트랜잭션 타임아웃 (ms)
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
}
)
Prisma Extensions
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient().$extends({
// 쿼리 확장 — 자동 소프트 삭제
query: {
user: {
async findMany({ args, query }) {
args.where = { ...args.where, deletedAt: null }
return query(args)
},
async delete({ args, query }) {
return prisma.user.update({
where: args.where,
data: { deletedAt: new Date() },
})
},
},
},
// 모델 확장 — 커스텀 메서드 추가
model: {
user: {
async findByEmail(email: string) {
return prisma.user.findUnique({ where: { email } })
},
async softDelete(id: number) {
return prisma.user.update({
where: { id },
data: { deletedAt: new Date() },
})
},
},
},
// 결과 확장 — 가상 필드 추가
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`
},
},
},
},
})
// 확장된 메서드 사용
const user = await prisma.user.findByEmail('alice@example.com')
const users = await prisma.user.findMany() // deletedAt: null 자동 적용
console.log(user?.fullName) // 가상 필드
페이지네이션
// 오프셋 기반 페이지네이션
async function getPaginatedUsers(page: number, limit: number) {
const [users, total] = await prisma.$transaction([
prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' },
}),
prisma.user.count(),
])
return {
data: users,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
}
}
// 커서 기반 페이지네이션 (무한 스크롤)
async function getPostsAfterCursor(cursor?: number, take = 10) {
const posts = await prisma.post.findMany({
take: take + 1, // 다음 페이지 여부 확인용으로 +1
...(cursor && {
cursor: { id: cursor },
skip: 1, // 커서 자체는 제외
}),
orderBy: { id: 'asc' },
})
const hasNextPage = posts.length > take
const data = hasNextPage ? posts.slice(0, -1) : posts
const nextCursor = hasNextPage ? data[data.length - 1].id : undefined
return { data, nextCursor, hasNextPage }
}
전체 텍스트 검색
// PostgreSQL 전문 검색
model Post {
id Int @id @default(autoincrement())
title String
content String
@@fulltext([title, content]) // MySQL/PlanetScale 전용
}
// PostgreSQL — contains + mode: insensitive
const results = await prisma.post.findMany({
where: {
OR: [
{ title: { contains: 'TypeScript', mode: 'insensitive' } },
{ content: { contains: 'TypeScript', mode: 'insensitive' } },
],
},
})
// MySQL — fulltext 검색
const results2 = await prisma.post.findMany({
where: {
OR: [
{ title: { search: 'TypeScript' } },
{ content: { search: 'TypeScript' } },
],
},
})
고수 팁
타입 안전한 동적 정렬
import { Prisma } from '@prisma/client'
type UserSortField = keyof Pick<Prisma.UserOrderByWithRelationInput, 'name' | 'email' | 'createdAt'>
async function getSortedUsers(sortBy: UserSortField, order: 'asc' | 'desc') {
return prisma.user.findMany({
orderBy: { [sortBy]: order },
})
}
// 사용
const users = await getSortedUsers('createdAt', 'desc')