Skip to main content
Advertisement

15.4 Prisma Advanced Queries — Raw SQL, Transactions, Extensions

Raw SQL

import { prisma } from './lib/prisma'
import { Prisma } from '@prisma/client'

// Tagged query (prevents SQL injection — use $queryRaw)
const email = 'alice@example.com'
const users = await prisma.$queryRaw<Array<{ id: number; name: string }>>`
SELECT id, name FROM "User" WHERE email = ${email}
`

// Dynamic parameters — use Prisma.sql tag
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}
`
// Convert BigInt to Number
const count = Number(result[0].count)

// Mutation query — $executeRaw
const affected = await prisma.$executeRaw`
UPDATE "Post" SET "viewCount" = "viewCount" + 1
WHERE id = ${1}
`
console.log(affected) // Number of affected rows

Transactions

Sequential Transaction

// Process multiple operations in one transaction
const [user, post] = await prisma.$transaction([
prisma.user.create({
data: { email: 'alice@example.com', name: 'Alice' },
}),
prisma.post.create({
data: { title: 'First Post', authorId: 1 },
}),
])

Interactive Transaction

// Handle conditional logic within a transaction
const result = await prisma.$transaction(async (tx) => {
// Debit
const sender = await tx.account.update({
where: { id: 'sender-id' },
data: { balance: { decrement: 100 } },
})

// Check balance
if (sender.balance < 0) {
throw new Error('Insufficient balance') // Auto-rollback
}

// Credit
const receiver = await tx.account.update({
where: { id: 'receiver-id' },
data: { balance: { increment: 100 } },
})

// Record transfer
await tx.transfer.create({
data: {
senderId: 'sender-id',
receiverId: 'receiver-id',
amount: 100,
},
})

return { sender, receiver }
})

// Transaction options
const result2 = await prisma.$transaction(
async (tx) => {
// ...
},
{
maxWait: 5000, // Max wait time (ms)
timeout: 10000, // Transaction timeout (ms)
isolationLevel: Prisma.TransactionIsolationLevel.Serializable,
}
)

Prisma Extensions

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient().$extends({
// Query extension — automatic soft delete
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 extension — add custom methods
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 extension — add virtual fields
result: {
user: {
fullName: {
needs: { firstName: true, lastName: true },
compute(user) {
return `${user.firstName} ${user.lastName}`
},
},
},
},
})

// Use extended methods
const user = await prisma.user.findByEmail('alice@example.com')
const users = await prisma.user.findMany() // Auto-applies deletedAt: null
console.log(user?.fullName) // Virtual field

Pagination

// Offset-based pagination
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,
}
}

// Cursor-based pagination (infinite scroll)
async function getPostsAfterCursor(cursor?: number, take = 10) {
const posts = await prisma.post.findMany({
take: take + 1, // Fetch +1 to check if next page exists
...(cursor && {
cursor: { id: cursor },
skip: 1, // Exclude the cursor itself
}),
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 full-text search
model Post {
id Int @id @default(autoincrement())
title String
content String

@@fulltext([title, content]) // MySQL/PlanetScale only
}
// PostgreSQL — contains + mode: insensitive
const results = await prisma.post.findMany({
where: {
OR: [
{ title: { contains: 'TypeScript', mode: 'insensitive' } },
{ content: { contains: 'TypeScript', mode: 'insensitive' } },
],
},
})

// MySQL — fulltext search
const results2 = await prisma.post.findMany({
where: {
OR: [
{ title: { search: 'TypeScript' } },
{ content: { search: 'TypeScript' } },
],
},
})

Pro Tips

Type-Safe Dynamic Sorting

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 },
})
}

// Usage
const users = await getSortedUsers('createdAt', 'desc')
Advertisement