Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:53 +08:00
commit f2cccfe864
10 changed files with 1544 additions and 0 deletions

View File

@@ -0,0 +1,412 @@
# Prisma Best Practices
## Schema Design
### Naming Conventions
**Models**: PascalCase, singular
```prisma
model User { } // [OK] Good
model users { } // [ERROR] Bad
```
**Fields**: camelCase
```prisma
model User {
createdAt DateTime // [OK] Good
created_at DateTime // [ERROR] Bad (unless mapping to existing DB)
}
```
**Database names**: snake_case with `@@map` and `@map`
```prisma
model Profile {
avatarUrl String @map("avatar_url")
@@map("profiles")
}
```
### ID Fields
**Prefer UUIDs for distributed systems:**
```prisma
id String @id @default(uuid()) @db.Uuid
```
**Use auto-increment for simple cases:**
```prisma
id Int @id @default(autoincrement())
```
### Timestamps
Always include creation and update timestamps:
```prisma
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
```
### Indexes
Add indexes on:
- Foreign keys (automatically indexed)
- Frequently queried fields
- Unique constraints
- Compound queries
```prisma
model Post {
authorId String @db.Uuid
published Boolean
publishedAt DateTime?
@@index([authorId])
@@index([published, publishedAt])
}
```
### Relations
**One-to-many:**
```prisma
model User {
posts Post[]
}
model Post {
authorId String @db.Uuid
author User @relation(fields: [authorId], references: [id])
@@index([authorId])
}
```
**Many-to-many:**
```prisma
model Post {
tags PostTag[]
}
model Tag {
posts PostTag[]
}
model PostTag {
postId String @db.Uuid
post Post @relation(fields: [postId], references: [id])
tagId String @db.Uuid
tag Tag @relation(fields: [tagId], references: [id])
@@id([postId, tagId])
}
```
**Cascade deletes:**
```prisma
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
```
## Query Optimization
### Select Only Needed Fields
```typescript
// [ERROR] Bad - fetches all fields
const users = await prisma.user.findMany();
// [OK] Good - only fetch what you need
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
email: true,
},
});
```
### Use Include Wisely
```typescript
// [ERROR] Bad - deep nesting can be slow
const posts = await prisma.post.findMany({
include: {
author: {
include: {
profile: {
include: {
settings: true,
},
},
},
},
},
});
// [OK] Good - only include what you display
const posts = await prisma.post.findMany({
include: {
author: {
select: {
id: true,
name: true,
},
},
},
});
```
### Pagination
Always paginate large result sets:
```typescript
// Offset pagination
const posts = await prisma.post.findMany({
skip: 20,
take: 10,
});
// Cursor-based pagination (better for large datasets)
const posts = await prisma.post.findMany({
take: 10,
cursor: {
id: lastPostId,
},
});
```
### Batch Operations
Use batch operations for bulk inserts/updates:
```typescript
// [OK] Single query instead of N queries
await prisma.user.createMany({
data: [
{ email: 'user1@example.com', name: 'User 1' },
{ email: 'user2@example.com', name: 'User 2' },
],
skipDuplicates: true,
});
```
## Connection Management
### Singleton Pattern (Next.js)
Prevent connection exhaustion in development:
```typescript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}
```
### Connection Pooling
Use Supabase connection pooler for better performance:
```prisma
datasource db {
provider = "postgresql"
url = env("DIRECT_URL") // Pooled connection
directUrl = env("DATABASE_URL") // Direct for migrations
}
```
## Migration Best Practices
### 1. Review Generated SQL
Always check `migration.sql` before applying:
```bash
npx prisma migrate dev --name add_users --create-only
# Review prisma/migrations/.../migration.sql
npx prisma migrate dev
```
### 2. Atomic Migrations
One logical change per migration:
- [OK] Good: "add_user_roles"
- [ERROR] Bad: "add_users_and_posts_and_comments"
### 3. Production Migrations
Use `migrate deploy` in production:
```bash
npx prisma migrate deploy
```
Never use `migrate dev` or `migrate reset` in production.
### 4. Handle Data Migrations
For complex data transformations, use multi-step migrations:
```sql
-- Step 1: Add new column with default
ALTER TABLE "User" ADD COLUMN "full_name" TEXT;
-- Step 2: Populate from existing data
UPDATE "User" SET "full_name" = "first_name" || ' ' || "last_name";
-- Step 3: Make non-nullable
ALTER TABLE "User" ALTER COLUMN "full_name" SET NOT NULL;
```
### 5. Rollback Strategy
Migrations can't be rolled back automatically. Plan for:
- Backup before major migrations
- Keep old columns temporarily
- Deploy in stages
## Error Handling
### Handle Unique Constraint Violations
```typescript
try {
await prisma.user.create({
data: { email: 'test@example.com' },
});
} catch (error) {
if (error.code === 'P2002') {
// Unique constraint violation
throw new Error('Email already exists');
}
throw error;
}
```
### Use Transactions
For operations that must succeed or fail together:
```typescript
await prisma.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'test@example.com' },
});
await tx.profile.create({
data: {
userId: user.id,
bio: 'New user',
},
});
});
```
## TypeScript Integration
### Leverage Generated Types
```typescript
import { Prisma } from '@prisma/client';
// Use generated types for input
type UserCreateInput = Prisma.UserCreateInput;
// Type-safe queries
const where: Prisma.UserWhereInput = {
email: {
contains: '@example.com',
},
};
```
### Type Query Results
```typescript
// Get inferred type from query
const user = await prisma.user.findUnique({
where: { id: '1' },
include: { posts: true },
});
type UserWithPosts = typeof user;
```
## Common Pitfalls
### 1. N+1 Query Problem
```typescript
// [ERROR] Bad - N+1 queries
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId },
});
}
// [OK] Good - single query with include
const posts = await prisma.post.findMany({
include: { author: true },
});
```
### 2. Not Using Transactions
```typescript
// [ERROR] Bad - can leave inconsistent state
await prisma.user.create({ data: userData });
await prisma.profile.create({ data: profileData }); // If this fails, user exists without profile
// [OK] Good - atomic operation
await prisma.$transaction([
prisma.user.create({ data: userData }),
prisma.profile.create({ data: profileData }),
]);
```
### 3. Exposing Prisma Client to Frontend
```typescript
// [ERROR] Bad - never import Prisma in client components
'use client';
import { prisma } from '@/lib/prisma'; // Security risk!
// [OK] Good - use Server Actions or API routes
'use server';
import { prisma } from '@/lib/prisma';
export async function getUsers() {
return await prisma.user.findMany();
}
```
### 4. Ignoring Soft Deletes
```prisma
model User {
deletedAt DateTime? @map("deleted_at")
}
// Query only non-deleted
const activeUsers = await prisma.user.findMany({
where: { deletedAt: null },
});
```
## Performance Tips
1. **Use indexes** on filtered/sorted fields
2. **Limit result sets** with take/skip
3. **Select only needed fields** to reduce data transfer
4. **Use connection pooling** for serverless environments
5. **Batch operations** instead of loops
6. **Cache frequent queries** at application level
7. **Monitor slow queries** in Supabase dashboard
8. **Use database views** for complex, repeated queries

View File

@@ -0,0 +1,294 @@
# Supabase Integration with Prisma
## Connection Configuration
### Database URLs
Supabase provides two types of connection strings:
**Direct Connection (Port 5432)** - For migrations:
```env
DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
```
**Pooled Connection (Port 6543)** - For queries via pgBouncer:
```env
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"
```
**Schema configuration:**
```prisma
datasource db {
provider = "postgresql"
url = env("DIRECT_URL") // Pooled for app queries
directUrl = env("DATABASE_URL") // Direct for migrations
}
```
### Why Two Connections?
- **Direct (5432)**: Supports all PostgreSQL features, required for migrations
- **Pooled (6543)**: Better performance, connection pooling, but limited features
- Prisma uses direct for migrations, pooled for queries
## Row Level Security (RLS) Considerations
### Prisma vs Supabase Auth
**Key concept**: Prisma connects as the `postgres` user, which **bypasses RLS policies**.
This means:
- Prisma can read/write all data regardless of RLS
- Use Prisma in Server Components/Actions where you control access
- RLS still protects data accessed via Supabase client
### Using Prisma with RLS
For RLS to work with Prisma, you need to set the user context:
```typescript
import { prisma } from '@/lib/prisma';
import { getCurrentUser } from '@/lib/auth/utils';
export async function getUserPosts() {
const user = await getCurrentUser();
// Set RLS context (requires custom configuration)
await prisma.$executeRaw`SET LOCAL rls.user_id = ${user.id}`;
// Now RLS policies can use current_setting('rls.user_id')
const posts = await prisma.post.findMany();
return posts;
}
```
**Better approach**: Use Prisma for admin operations, Supabase client for user operations:
```typescript
// Admin operation - bypasses RLS
import { prisma } from '@/lib/prisma';
await prisma.post.findMany(); // Gets all posts
// User operation - respects RLS
import { createServerClient } from '@/lib/supabase/server';
const supabase = await createServerClient();
const { data } = await supabase.from('posts').select('*'); // Only user's posts
```
## Schema Management
### Prisma Manages Schema
Use Prisma as source of truth for schema:
```bash
# 1. Update schema.prisma
# 2. Generate migration
npx prisma migrate dev --name add_posts
# 3. Migration is applied to Supabase database
```
### Supabase Features
Some Supabase features are managed outside Prisma:
**RLS Policies** - Define in Supabase dashboard or SQL:
```sql
CREATE POLICY "Users can view own posts"
ON posts FOR SELECT
USING (auth.uid() = author_id);
```
**Storage Buckets** - Use Supabase dashboard/API
**Realtime** - Configure in Supabase dashboard
**Edge Functions** - Deploy separately
### Hybrid Approach
1. Use Prisma for schema, migrations, and admin operations
2. Use Supabase client for user-facing operations with RLS
3. Define RLS policies separately from Prisma
## Working with Supabase Auth
### Linking to auth.users
Don't create foreign key to `auth.users` (different schema):
```prisma
model Profile {
id String @id @db.Uuid // Same as auth.users.id
email String @unique
// No foreign key to auth.users - different schema
}
```
**Create profile on signup** via database trigger:
```sql
-- Create profile when user signs up
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, email)
VALUES (NEW.id, NEW.email);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW
EXECUTE FUNCTION public.handle_new_user();
```
### Syncing Profile Data
Keep profiles in sync with auth.users:
```sql
-- Update profile when email changes
CREATE OR REPLACE FUNCTION public.handle_user_update()
RETURNS TRIGGER AS $$
BEGIN
UPDATE public.profiles
SET email = NEW.email
WHERE id = NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_updated
AFTER UPDATE ON auth.users
FOR EACH ROW
WHEN (OLD.email IS DISTINCT FROM NEW.email)
EXECUTE FUNCTION public.handle_user_update();
```
## Migrations in Supabase
### Local Development
1. Pull current schema from Supabase:
```bash
npx prisma db pull
```
2. Make changes to schema.prisma
3. Create migration:
```bash
npx prisma migrate dev --name my_changes
```
### Production Deployment
Apply migrations during deployment:
```bash
npx prisma migrate deploy
```
**GitHub Actions example:**
```yaml
- name: Run Prisma migrations
run: npx prisma migrate deploy
env:
DATABASE_URL: ${{ secrets.SUPABASE_DATABASE_URL }}
```
### Migration History
Prisma creates `_prisma_migrations` table to track applied migrations. Don't modify this table.
## Realtime with Prisma
Supabase Realtime works with Prisma-managed tables:
1. Create table via Prisma migration
2. Enable realtime in Supabase dashboard for specific tables
3. Subscribe to changes via Supabase client
```typescript
// Enable realtime on a table (in SQL editor or dashboard)
ALTER PUBLICATION supabase_realtime ADD TABLE posts;
// Subscribe to changes
const supabase = createClient();
const channel = supabase
.channel('posts')
.on('postgres_changes', { event: '*', schema: 'public', table: 'posts' },
(payload) => {
console.log('Change received!', payload);
}
)
.subscribe();
```
## Environment Setup
### Development
```env
# .env.local
DATABASE_URL="postgresql://postgres:postgres@localhost:54322/postgres"
DIRECT_URL="postgresql://postgres:postgres@localhost:54322/postgres"
```
### Staging/Production
```env
# .env.production
DATABASE_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:5432/postgres"
DIRECT_URL="postgresql://postgres:[PASSWORD]@db.[PROJECT-REF].supabase.co:6543/postgres?pgbouncer=true"
```
## Best Practices
1. **Use Prisma for schema management** - Single source of truth
2. **Use Supabase client for RLS-protected queries** - User-facing operations
3. **Use Prisma for admin operations** - Bulk updates, analytics
4. **Define RLS policies separately** - Not managed by Prisma
5. **Use triggers for auth.users integration** - Auto-create profiles
6. **Enable realtime selectively** - Only on tables that need it
7. **Test migrations locally first** - Use Supabase CLI for local dev
8. **Monitor connection pool** - Use pooled connection for queries
## Troubleshooting
**Migration fails with permission error**: Ensure DATABASE_URL uses postgres user with sufficient privileges.
**RLS blocks Prisma queries**: This is expected. Use Supabase client for RLS-protected data.
**Connection pool exhausted**: Use pooled connection (DIRECT_URL) for application queries.
**Realtime not working**: Check table is published (`ALTER PUBLICATION supabase_realtime ADD TABLE tablename`).
**Auth user ID doesn't match profile**: Ensure trigger exists and is executed on user creation.
## Supabase CLI Integration
Use Supabase CLI for local development:
```bash
# Start local Supabase
npx supabase start
# Link to remote project
npx supabase link --project-ref your-project-ref
# Pull remote schema
npx supabase db pull
# Generate types for Supabase client
npx supabase gen types typescript --local > types/database.ts
```
Prisma and Supabase CLI can coexist:
- Prisma for schema management and migrations
- Supabase CLI for local development and type generation