본문으로 건너뛰기
Advertisement

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')
Advertisement