Prisma Client & PostgreSQL
Overviewβ
QMS uses Prisma as the ORM for type-safe database access and PostgreSQL as the primary database. Prisma provides auto-generated, type-safe database client and schema migrations.
What is Prisma?β
Prisma is:
- Type-Safe: Generated client with full TypeScript support
- Developer-Friendly: Intuitive query API
- Schema-Driven: Single source of truth for database schema
- Migration Management: Version-controlled database changes
- Multi-Database: Supports PostgreSQL, MySQL, SQLite, MongoDB, etc.
- Performance: Query optimization and connection pooling
Setupβ
Installationβ
pnpm add @prisma/client
pnpm add -D prisma
Initialize Prismaβ
pnpm exec prisma init
# Creates:
# - prisma/schema.prisma (schema file)
# - .env (environment variables)
Schema Definitionβ
Basic Schemaβ
prisma/schema.prismaβ
// Prisma schema file. Learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Define models below...
Employee Model Exampleβ
model Employee {
/// Unique identifier
id String @id @default(cuid())
name String
email String @unique
department String
role String
isActive Boolean @default(true)
phone String? // Optional field
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // For soft deletes
// Relations
sessions UserSession[]
@@map("tb_m_employee") // Database table name
@@index([email]) // Database index
}
model UserSession {
id String @id @default(cuid())
userId String
user Employee @relation(fields: [userId], references: [id], onDelete: Cascade)
token String @unique
expiresAt DateTime
createdAt DateTime @default(now())
@@map("tb_r_user_session")
@@unique([userId, token])
}
model System {
id String @id @default(cuid())
name String
description String?
status String @default("active")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("tb_m_system")
}
Advanced Schema Featuresβ
Enumsβ
enum UserRole {
ADMIN
MANAGER
EMPLOYEE
VIEWER
@@map("user_role_enum")
}
model User {
id String @id @default(cuid())
role UserRole @default(EMPLOYEE)
}
Composite Unique Constraintsβ
model UserDepartment {
userId String
departmentId String
joinedAt DateTime @default(now())
@@unique([userId, departmentId])
@@map("user_departments")
}
Relations - One to Manyβ
model Department {
id String @id @default(cuid())
name String
employees Employee[] // One-to-many relation
}
model Employee {
id String @id @default(cuid())
name String
departmentId String
department Department @relation(fields: [departmentId], references: [id])
}
Relations - Many to Manyβ
model User {
id String @id @default(cuid())
name String
roles Role[] @relation("UserRoles")
}
model Role {
id String @id @default(cuid())
name String
users User[] @relation("UserRoles")
@@map("roles")
}
Environment Configurationβ
.envβ
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/qms?schema=public"
# Connection pool
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
DATABASE_POOL_IDLE_TIMEOUT=30000
Connection String Formatβ
postgresql://[user[:password]@][netlocation][:port][/dbname][?param1=value1&...]
Database Operationsβ
PrismaServiceβ
src/prisma/prisma.service.tsβ
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
}
Basic Operationsβ
// Create
await prisma.employee.create({
data: {
name: 'John Doe',
email: 'john@example.com',
department: 'Engineering',
},
});
// Read
await prisma.employee.findUnique({
where: { id: '123' },
});
// Read many
await prisma.employee.findMany({
where: { department: 'Engineering' },
skip: 0,
take: 10,
orderBy: { createdAt: 'desc' },
});
// Update
await prisma.employee.update({
where: { id: '123' },
data: { department: 'Management' },
});
// Delete
await prisma.employee.delete({
where: { id: '123' },
});
// Delete many
await prisma.employee.deleteMany({
where: { isActive: false },
});
Complex Queriesβ
// With relations
const employee = await prisma.employee.findUnique({
where: { id: '123' },
include: {
sessions: true,
department: true,
},
});
// Conditional filtering
const employees = await prisma.employee.findMany({
where: {
AND: [
{ department: 'Engineering' },
{ isActive: true },
{
OR: [
{ name: { contains: 'John', mode: 'insensitive' } },
{ email: { contains: 'john', mode: 'insensitive' } },
],
},
],
},
});
// Projections (select specific fields)
const names = await prisma.employee.findMany({
select: {
id: true,
name: true,
email: true,
},
});
// Aggregations
const count = await prisma.employee.count({
where: { department: 'Engineering' },
});
const count = await prisma.employee.aggregate({
where: { department: 'Engineering' },
_count: true,
});
Transactionsβ
// Atomic operations
await prisma.$transaction(async (tx) => {
// These operations all succeed or all fail
const employee = await tx.employee.create({
data: { name: 'John', email: 'john@example.com' },
});
await tx.userSession.create({
data: {
userId: employee.id,
token: 'token123',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000),
},
});
return employee;
});
Migrationsβ
Create Migrationβ
# Create migration from schema changes
pnpm exec prisma migrate dev --name add_phone_to_employee
# Generates:
# - prisma/migrations/[timestamp]_add_phone_to_employee/
# βββ migration.sql
# Applies migrations and regenerates Prisma Client
Migration Filesβ
Migrations are SQL-based and version-controlled:
-- prisma/migrations/20240115_add_phone/migration.sql
ALTER TABLE "tb_m_employee" ADD COLUMN "phone" TEXT;
CREATE INDEX "idx_phone" ON "tb_m_employee"("phone");
Reset Databaseβ
# Reset (WARNING: destructive!)
pnpm exec prisma migrate reset
# Drops database and re-runs all migrations
Deployment Migrationsβ
# Apply migrations without prompting (for CI/CD)
pnpm exec prisma migrate deploy
Seedingβ
Seed Scriptβ
prisma/seed.tsβ
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Clear existing data
await prisma.employee.deleteMany();
// Seed data
const emp1 = await prisma.employee.create({
data: {
name: 'Alice Johnson',
email: 'alice@example.com',
department: 'Engineering',
role: 'Senior Developer',
},
});
const emp2 = await prisma.employee.create({
data: {
name: 'Bob Smith',
email: 'bob@example.com',
department: 'Sales',
role: 'Sales Manager',
},
});
console.log('Seed data created:', emp1, emp2);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
Run Seedβ
# Setup in package.json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
# Run seed
pnpm exec prisma db seed
# Run on migration reset
pnpm exec prisma migrate reset
Query Time Debuggingβ
Enable Query Loggingβ
// In PrismaService or main.ts
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
// Or with event listeners
prisma.$on('query', (e) => {
console.log('Query: ' + e.query);
console.log('Params: ' + JSON.stringify(e.params));
console.log('Duration: ' + e.duration + 'ms');
});
Performance Optimizationβ
Connection Poolingβ
# In DATABASE_URL
postgresql://user:pass@localhost:5432/qms?schema=public&connection_limit=10
Select Only Needed Fieldsβ
// β
GOOD: Select specific fields
const employees = await prisma.employee.findMany({
select: {
id: true,
name: true,
email: true,
},
});
// β AVOID: Fetch all fields
const employees = await prisma.employee.findMany();
Batch Operationsβ
// β
GOOD: Batch update
await prisma.employee.updateMany({
where: { department: 'Engineering' },
data: { department: 'Management' },
});
// β AVOID: Loop and update each
for (const emp of employees) {
await prisma.employee.update({
where: { id: emp.id },
data: { department: 'Management' },
});
}
Use Paginationβ
const pageSize = 20;
const page = 1;
const employees = await prisma.employee.findMany({
skip: (page - 1) * pageSize,
take: pageSize,
orderBy: { createdAt: 'desc' },
});
Prisma Studioβ
View Data Visuallyβ
# Launch Prisma Studio
pnpm exec prisma studio
# Opens http://localhost:5555
Type Generationβ
Auto-Generated Typesβ
Prisma generates types automatically:
import { Employee, Prisma } from '@prisma/client';
// Full type
const employee: Employee = {
/* ... */
};
// Input type
const createData: Prisma.EmployeeCreateInput = {
name: 'John',
email: 'john@example.com',
};
// Type for database response
const result: Prisma.EmployeeGetPayload<{
include: { sessions: true };
}>;
Best Practicesβ
1. Use Transactions for Related Operationsβ
// β
GOOD: Atomic
await prisma.$transaction(async (tx) => {
const user = await tx.employee.create({
/* ... */
});
await tx.userSession.create({
data: { userId: user.id /* ... */ },
});
});
// β AVOID: Not atomic
const user = await prisma.employee.create({
/* ... */
});
await prisma.userSession.create({
data: { userId: user.id /* ... */ },
});
2. Use Where Clauses Effectivelyβ
// β
GOOD: Filter at database
const engineers = await prisma.employee.findMany({
where: { department: 'Engineering' },
});
// β AVOID: Filter in application
const all = await prisma.employee.findMany();
const engineers = all.filter((e) => e.department === 'Engineering');
3. Validate Foreign Keys Firstβ
// β
GOOD: Check FK exists
const department = await prisma.department.findUnique({
where: { id: deptId },
});
if (!department) {
throw new NotFoundException('Department not found');
}
// β AVOID: Rely on DB error
try {
await prisma.employee.create({
data: { /* ... */, departmentId: invalidId },
});
} catch (e) {
// Handle FK error
}
4. Plan Indexesβ
// Index for lookups
model Employee {
id String @id
email String @unique // Creates index automatically
name String
@@index([name]) // Add index for name searches
}
Troubleshootingβ
Connection Issuesβ
# Test connection
pnpm exec prisma db execute --stdin < /dev/null
# Check database
psql postgresql://user:pass@localhost:5432/qms
# Generate client
pnpm exec prisma generate
Type Errorsβ
# Regenerate types
pnpm exec prisma generate
# Check schema validity
pnpm exec prisma validate