Prisma - Kompletny przewodnik po ORM nowej generacji dla TypeScript
Czym jest Prisma i dlaczego zmienia podejście do baz danych?
Prisma to ORM (Object-Relational Mapping) nowej generacji dla Node.js i TypeScript. W przeciwieństwie do tradycyjnych ORM-ów jak Sequelize czy TypeORM, Prisma generuje klienta TypeScript bezpośrednio ze schematu bazy danych, zapewniając pełne type safety i świetne autouzupełnianie w IDE.
Prisma składa się z trzech głównych komponentów:
- Prisma Client - Auto-generowany, type-safe query builder
- Prisma Migrate - System migracji bazujący na schema
- Prisma Studio - GUI do przeglądania i edycji danych
Dlaczego Prisma wygrała z alternatywami?
Type Safety na sterydach
// Tradycyjny ORM - brak type safety
const user = await User.findOne({ where: { id: 1 } })
user.nmae // Literówka - brak błędu w czasie kompilacji!
// Prisma - pełne type safety
const user = await prisma.user.findUnique({ where: { id: 1 } })
user.nmae // ❌ Error: Property 'nmae' does not exist on type 'User'
user.name // ✅ Działa z autouzupełnianiemIntuicyjne API
// Prisma ma czytelne, chainable API
const posts = await prisma.post.findMany({
where: {
author: {
email: { contains: '@example.com' }
},
published: true
},
include: {
author: true,
comments: {
take: 5,
orderBy: { createdAt: 'desc' }
}
},
orderBy: { createdAt: 'desc' },
take: 10
})Instalacja i setup
Nowy projekt
# Inicjalizacja projektu
npm init -y
npm install typescript ts-node @types/node --save-dev
npm install prisma --save-dev
npm install @prisma/client
# Inicjalizacja Prisma
npx prisma initStruktura po inicjalizacji
my-project/
├── prisma/
│ └── schema.prisma # Schema bazy danych
├── .env # Zmienne środowiskowe (DATABASE_URL)
└── package.jsonSchema Prisma - Serce projektu
Podstawowa struktura
// prisma/schema.prisma
// Generator - jak generować klienta
generator client {
provider = "prisma-client-js"
}
// Datasource - połączenie z bazą
datasource db {
provider = "postgresql" // postgresql, mysql, sqlite, sqlserver, mongodb
url = env("DATABASE_URL")
}
// Modele - tabele w bazie
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
role Role @default(USER)
posts Post[]
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
tags Tag[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
}
model Profile {
id Int @id @default(autoincrement())
bio String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @unique
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
model Comment {
id Int @id @default(autoincrement())
content String
post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
postId Int
createdAt DateTime @default(now())
}
enum Role {
USER
ADMIN
MODERATOR
}Typy pól
model Example {
// Podstawowe typy
stringField String
intField Int
floatField Float
boolField Boolean
dateField DateTime
// Opcjonalne pole (może być null)
optionalField String?
// Lista
tags String[]
// JSON (PostgreSQL, MySQL)
metadata Json
// Decimal dla finansów
price Decimal @db.Decimal(10, 2)
// BigInt dla dużych liczb
bigNumber BigInt
// Bytes dla plików binarnych
data Bytes
}Atrybuty pól
model User {
id Int @id @default(autoincrement()) // Primary key + auto increment
uuid String @id @default(uuid()) // UUID jako primary key
cuid String @id @default(cuid()) // CUID jako primary key
email String @unique // Unikalny
role Role @default(USER) // Wartość domyślna
createdAt DateTime @default(now()) // Timestamp
updatedAt DateTime @updatedAt // Auto-update timestamp
// Mapowanie nazwy kolumny w bazie
firstName String @map("first_name")
// Mapowanie nazwy tabeli
@@map("users")
// Indeksy
@@index([email])
@@index([firstName, lastName])
// Composite unique
@@unique([email, tenantId])
}Relacje
// One-to-One
model User {
id Int @id @default(autoincrement())
profile Profile?
}
model Profile {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id])
userId Int @unique // unique = one-to-one
}
// 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
}
// Many-to-Many (implicit)
model Post {
id Int @id @default(autoincrement())
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
posts Post[]
}
// Many-to-Many (explicit - z dodatkowymi polami)
model Post {
id Int @id @default(autoincrement())
tags PostTag[]
}
model Tag {
id Int @id @default(autoincrement())
posts 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])
}
// Self-relation (np. followers)
model User {
id Int @id @default(autoincrement())
followers User[] @relation("UserFollows")
following User[] @relation("UserFollows")
}Migracje
Podstawowe komendy
# Utwórz migrację z aktualnego schematu
npx prisma migrate dev --name init
# Zastosuj migracje w produkcji
npx prisma migrate deploy
# Reset bazy (UWAGA: usuwa wszystkie dane)
npx prisma migrate reset
# Wygeneruj klienta bez migracji
npx prisma generate
# Formatuj schema
npx prisma format
# Waliduj schema
npx prisma validate
# Otwórz Prisma Studio
npx prisma studioWorkflow migracji
# 1. Zmień schema.prisma
# 2. Utwórz migrację
npx prisma migrate dev --name add_user_avatar
# 3. Sprawdź wygenerowany SQL w prisma/migrations/
# 4. Commit zmiany do repozytoriumPrisma Client - Query Builder
Inicjalizacja klienta
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
log: process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
})
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma
}CRUD Operations
import { prisma } from '@/lib/prisma'
// CREATE
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
profile: {
create: { bio: 'Software developer' }
},
posts: {
create: [
{ title: 'Hello World' },
{ title: 'My Second Post' }
]
}
},
include: {
profile: true,
posts: true
}
})
// CREATE MANY
const users = await prisma.user.createMany({
data: [
{ email: 'bob@example.com', name: 'Bob' },
{ email: 'charlie@example.com', name: 'Charlie' }
],
skipDuplicates: true
})
// READ - findUnique (by unique field)
const user = await prisma.user.findUnique({
where: { email: 'alice@example.com' }
})
// READ - findFirst
const firstAdmin = await prisma.user.findFirst({
where: { role: 'ADMIN' },
orderBy: { createdAt: 'asc' }
})
// READ - findMany
const users = await prisma.user.findMany({
where: {
email: { contains: '@example.com' },
role: { in: ['USER', 'ADMIN'] },
posts: { some: { published: true } }
},
orderBy: [
{ role: 'asc' },
{ name: 'asc' }
],
skip: 0,
take: 10
})
// UPDATE
const updatedUser = await prisma.user.update({
where: { email: 'alice@example.com' },
data: {
name: 'Alice Smith',
posts: {
updateMany: {
where: { published: false },
data: { published: true }
}
}
}
})
// UPDATE MANY
const result = await prisma.post.updateMany({
where: { authorId: 1, published: false },
data: { published: true }
})
// UPSERT
const user = await prisma.user.upsert({
where: { email: 'alice@example.com' },
update: { name: 'Alice Updated' },
create: { email: 'alice@example.com', name: 'Alice' }
})
// DELETE
const deletedUser = await prisma.user.delete({
where: { id: 1 }
})
// DELETE MANY
const result = await prisma.post.deleteMany({
where: { published: false }
})Zaawansowane filtry
// Operatory porównania
const posts = await prisma.post.findMany({
where: {
viewCount: { gt: 100 }, // greater than
likes: { gte: 10 }, // greater than or equal
comments: { lt: 50 }, // less than
shares: { lte: 5 }, // less than or equal
title: { not: 'Draft' }, // not equal
authorId: { in: [1, 2, 3] }, // in array
status: { notIn: ['DELETED'] } // not in array
}
})
// String filters
const users = await prisma.user.findMany({
where: {
email: { contains: '@gmail.com' },
name: { startsWith: 'A' },
bio: { endsWith: 'developer' },
// Case insensitive (PostgreSQL)
name: { contains: 'alice', mode: 'insensitive' }
}
})
// Relacje
const usersWithPosts = await prisma.user.findMany({
where: {
// Ma przynajmniej jeden post
posts: { some: { published: true } },
// Wszystkie posty są opublikowane
posts: { every: { published: true } },
// Nie ma postów
posts: { none: {} },
// Sprawdź pola w relacji
posts: {
some: {
title: { contains: 'Prisma' },
comments: { some: {} }
}
}
}
})
// AND, OR, NOT
const posts = await prisma.post.findMany({
where: {
AND: [
{ published: true },
{ authorId: 1 }
],
OR: [
{ title: { contains: 'Prisma' } },
{ content: { contains: 'Prisma' } }
],
NOT: {
status: 'DELETED'
}
}
})Select i Include
// Select - wybierz tylko określone pola
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
_count: {
select: { posts: true }
}
}
})
// Include - dołącz relacje
const users = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
orderBy: { createdAt: 'desc' },
take: 5,
include: {
comments: true
}
},
profile: true
}
})
// Nested select w include
const users = await prisma.user.findMany({
include: {
posts: {
select: {
id: true,
title: true
}
}
}
})Agregacje
// Count
const userCount = await prisma.user.count({
where: { role: 'ADMIN' }
})
// Aggregate
const stats = await prisma.post.aggregate({
_count: { _all: true },
_avg: { viewCount: true },
_sum: { viewCount: true },
_min: { viewCount: true },
_max: { viewCount: true },
where: { published: true }
})
// Group by
const postsByAuthor = await prisma.post.groupBy({
by: ['authorId'],
_count: { _all: true },
_avg: { viewCount: true },
having: {
viewCount: { _avg: { gt: 100 } }
},
orderBy: {
_count: { _all: 'desc' }
}
})Transakcje
// Interactive transaction
const [user, post] = await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'new@example.com', name: 'New User' }
})
const post = await tx.post.create({
data: {
title: 'Welcome Post',
authorId: user.id
}
})
// Jeśli cokolwiek się nie powiedzie, wszystko zostanie wycofane
return [user, post]
})
// Sequential transaction (array of operations)
const [deletedPosts, updatedUser] = await prisma.$transaction([
prisma.post.deleteMany({ where: { authorId: 1 } }),
prisma.user.update({
where: { id: 1 },
data: { postCount: 0 }
})
])Raw Queries
// Raw SQL query
const users = await prisma.$queryRaw`
SELECT * FROM "User"
WHERE email LIKE ${`%@example.com`}
ORDER BY "createdAt" DESC
LIMIT 10
`
// Raw SQL execute
const result = await prisma.$executeRaw`
UPDATE "Post"
SET "viewCount" = "viewCount" + 1
WHERE id = ${postId}
`
// Tagged template literal (bezpieczne od SQL injection)
const email = 'alice@example.com'
const user = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${email}
`Integracja z Next.js
Server Components
// app/users/page.tsx
import { prisma } from '@/lib/prisma'
export default async function UsersPage() {
const users = await prisma.user.findMany({
include: { _count: { select: { posts: true } } }
})
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} ({user._count.posts} posts)
</li>
))}
</ul>
)
}Server Actions
// app/users/actions.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createUser(formData: FormData) {
const email = formData.get('email') as string
const name = formData.get('name') as string
await prisma.user.create({
data: { email, name }
})
revalidatePath('/users')
}
export async function deleteUser(id: number) {
await prisma.user.delete({ where: { id } })
revalidatePath('/users')
}API Routes
// app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = 10
const [users, total] = await prisma.$transaction([
prisma.user.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
}),
prisma.user.count()
])
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
})
}
export async function POST(request: Request) {
const body = await request.json()
const user = await prisma.user.create({
data: body
})
return NextResponse.json(user, { status: 201 })
}
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const user = await prisma.user.findUnique({
where: { id: parseInt(params.id) },
include: { posts: true, profile: true }
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json(user)
}Seeding
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function main() {
// Usuń istniejące dane
await prisma.post.deleteMany()
await prisma.user.deleteMany()
// Utwórz użytkowników
const alice = await prisma.user.create({
data: {
email: 'alice@example.com',
name: 'Alice',
role: 'ADMIN',
profile: {
create: { bio: 'Admin user' }
},
posts: {
create: [
{ title: 'Hello World', published: true },
{ title: 'Draft Post', published: false }
]
}
}
})
const bob = await prisma.user.create({
data: {
email: 'bob@example.com',
name: 'Bob',
posts: {
create: { title: 'My First Post', published: true }
}
}
})
console.log({ alice, bob })
}
main()
.catch((e) => {
console.error(e)
process.exit(1)
})
.finally(async () => {
await prisma.$disconnect()
})// package.json
{
"prisma": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
}npx prisma db seedPrisma vs Alternatywy
| Aspekt | Prisma | TypeORM | Drizzle |
|---|---|---|---|
| Type Safety | Pełne (generowane) | Częściowe | Pełne |
| API | Query builder | Active Record + Query builder | SQL-like |
| Migracje | Deklaratywne | Imperatywne | SQL |
| Performance | Dobra | Dobra | Bardzo dobra |
| Learning Curve | Niska | Średnia | Niska |
| Flexibility | Średnia | Wysoka | Wysoka |
Best Practices
1. Singleton pattern dla klienta
// ZAWSZE używaj singleton pattern w Next.js
// Zapobiega tworzeniu wielu połączeń w development
export const prisma = globalForPrisma.prisma ?? new PrismaClient()2. Soft delete zamiast hard delete
model Post {
id Int @id @default(autoincrement())
deletedAt DateTime?
// ...
}// "Usuń" post
await prisma.post.update({
where: { id: 1 },
data: { deletedAt: new Date() }
})
// Pobierz tylko nieusunięte
const posts = await prisma.post.findMany({
where: { deletedAt: null }
})3. Middleware dla audit log
const prisma = new PrismaClient().$extends({
query: {
$allModels: {
async $allOperations({ model, operation, args, query }) {
const start = Date.now()
const result = await query(args)
const duration = Date.now() - start
console.log(`${model}.${operation} took ${duration}ms`)
return result
}
}
}
})4. Pagination helper
interface PaginationParams {
page?: number
limit?: number
}
async function paginate<T>(
model: any,
{ page = 1, limit = 10 }: PaginationParams,
args: any = {}
) {
const [data, total] = await prisma.$transaction([
model.findMany({
...args,
skip: (page - 1) * limit,
take: limit
}),
model.count({ where: args.where })
])
return {
data,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
}
}
// Użycie
const result = await paginate(prisma.user, { page: 2, limit: 20 }, {
where: { role: 'USER' },
orderBy: { createdAt: 'desc' }
})FAQ
Czy Prisma obsługuje MongoDB?
Tak, od wersji 3.12. Składnia jest bardzo podobna, ale niektóre funkcje (jak transakcje) wymagają replica set.
Jak debugować queries?
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error']
})Jak wykonać batch operations?
// createMany, updateMany, deleteMany
const result = await prisma.user.createMany({
data: users,
skipDuplicates: true
})Czy Prisma jest szybka?
Prisma jest wystarczająco szybka dla większości zastosowań. Dla ekstremalnie wydajnych queries, możesz użyć $queryRaw.
Podsumowanie
Prisma zmienia sposób w jaki pracujemy z bazami danych w Node.js/TypeScript:
- Type Safety - Błędy łapane w czasie kompilacji
- DX - Świetne autouzupełnianie i dokumentacja inline
- Migracje - Proste, deklaratywne migracje
- Prisma Studio - GUI bez pisania kodu