225 lines
4.8 KiB
Markdown
225 lines
4.8 KiB
Markdown
# Database Patterns - Prisma Best Practices
|
|
|
|
Complete guide to database access patterns using Prisma in backend microservices.
|
|
|
|
## Table of Contents
|
|
|
|
- [PrismaService Usage](#prismaservice-usage)
|
|
- [Repository Pattern](#repository-pattern)
|
|
- [Transaction Patterns](#transaction-patterns)
|
|
- [Query Optimization](#query-optimization)
|
|
- [N+1 Query Prevention](#n1-query-prevention)
|
|
- [Error Handling](#error-handling)
|
|
|
|
---
|
|
|
|
## PrismaService Usage
|
|
|
|
### Basic Pattern
|
|
|
|
```typescript
|
|
import { PrismaService } from '@project-lifecycle-portal/database';
|
|
|
|
// Always use PrismaService.main
|
|
const users = await PrismaService.main.user.findMany();
|
|
```
|
|
|
|
### Check Availability
|
|
|
|
```typescript
|
|
if (!PrismaService.isAvailable) {
|
|
throw new Error('Prisma client not initialized');
|
|
}
|
|
|
|
const user = await PrismaService.main.user.findUnique({ where: { id } });
|
|
```
|
|
|
|
---
|
|
|
|
## Repository Pattern
|
|
|
|
### Why Use Repositories
|
|
|
|
✅ **Use repositories when:**
|
|
- Complex queries with joins/includes
|
|
- Query used in multiple places
|
|
- Need caching layer
|
|
- Want to mock for testing
|
|
|
|
❌ **Skip repositories for:**
|
|
- Simple one-off queries
|
|
- Prototyping (can refactor later)
|
|
|
|
### Repository Template
|
|
|
|
```typescript
|
|
export class UserRepository {
|
|
async findById(id: string): Promise<User | null> {
|
|
return PrismaService.main.user.findUnique({
|
|
where: { id },
|
|
include: { profile: true },
|
|
});
|
|
}
|
|
|
|
async findActive(): Promise<User[]> {
|
|
return PrismaService.main.user.findMany({
|
|
where: { isActive: true },
|
|
orderBy: { createdAt: 'desc' },
|
|
});
|
|
}
|
|
|
|
async create(data: Prisma.UserCreateInput): Promise<User> {
|
|
return PrismaService.main.user.create({ data });
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Transaction Patterns
|
|
|
|
### Simple Transaction
|
|
|
|
```typescript
|
|
const result = await PrismaService.main.$transaction(async (tx) => {
|
|
const user = await tx.user.create({ data: userData });
|
|
const profile = await tx.userProfile.create({ data: { userId: user.id } });
|
|
return { user, profile };
|
|
});
|
|
```
|
|
|
|
### Interactive Transaction
|
|
|
|
```typescript
|
|
const result = await PrismaService.main.$transaction(
|
|
async (tx) => {
|
|
const user = await tx.user.findUnique({ where: { id } });
|
|
if (!user) throw new Error('User not found');
|
|
|
|
return await tx.user.update({
|
|
where: { id },
|
|
data: { lastLogin: new Date() },
|
|
});
|
|
},
|
|
{
|
|
maxWait: 5000,
|
|
timeout: 10000,
|
|
}
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Query Optimization
|
|
|
|
### Use select to Limit Fields
|
|
|
|
```typescript
|
|
// ❌ Fetches all fields
|
|
const users = await PrismaService.main.user.findMany();
|
|
|
|
// ✅ Only fetch needed fields
|
|
const users = await PrismaService.main.user.findMany({
|
|
select: {
|
|
id: true,
|
|
email: true,
|
|
profile: { select: { firstName: true, lastName: true } },
|
|
},
|
|
});
|
|
```
|
|
|
|
### Use include Carefully
|
|
|
|
```typescript
|
|
// ❌ Excessive includes
|
|
const user = await PrismaService.main.user.findUnique({
|
|
where: { id },
|
|
include: {
|
|
profile: true,
|
|
posts: { include: { comments: true } },
|
|
workflows: { include: { steps: { include: { actions: true } } } },
|
|
},
|
|
});
|
|
|
|
// ✅ Only include what you need
|
|
const user = await PrismaService.main.user.findUnique({
|
|
where: { id },
|
|
include: { profile: true },
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## N+1 Query Prevention
|
|
|
|
### Problem: N+1 Queries
|
|
|
|
```typescript
|
|
// ❌ N+1 Query Problem
|
|
const users = await PrismaService.main.user.findMany(); // 1 query
|
|
|
|
for (const user of users) {
|
|
// N queries (one per user)
|
|
const profile = await PrismaService.main.userProfile.findUnique({
|
|
where: { userId: user.id },
|
|
});
|
|
}
|
|
```
|
|
|
|
### Solution: Use include or Batching
|
|
|
|
```typescript
|
|
// ✅ Single query with include
|
|
const users = await PrismaService.main.user.findMany({
|
|
include: { profile: true },
|
|
});
|
|
|
|
// ✅ Or batch query
|
|
const userIds = users.map(u => u.id);
|
|
const profiles = await PrismaService.main.userProfile.findMany({
|
|
where: { userId: { in: userIds } },
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## Error Handling
|
|
|
|
### Prisma Error Types
|
|
|
|
```typescript
|
|
import { Prisma } from '@prisma/client';
|
|
|
|
try {
|
|
await PrismaService.main.user.create({ data });
|
|
} catch (error) {
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
// Unique constraint violation
|
|
if (error.code === 'P2002') {
|
|
throw new ConflictError('Email already exists');
|
|
}
|
|
|
|
// Foreign key constraint
|
|
if (error.code === 'P2003') {
|
|
throw new ValidationError('Invalid reference');
|
|
}
|
|
|
|
// Record not found
|
|
if (error.code === 'P2025') {
|
|
throw new NotFoundError('Record not found');
|
|
}
|
|
}
|
|
|
|
// Unknown error
|
|
Sentry.captureException(error);
|
|
throw error;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
**Related Files:**
|
|
- SKILL.md
|
|
- [services-and-repositories.md](services-and-repositories.md)
|
|
- [async-and-errors.md](async-and-errors.md)
|