--- description: Prisma 数据库设计规范和最佳实践 globs: - "backend/prisma/**/*.prisma" alwaysApply: false --- # 数据库设计规范 ## Prisma Schema 规范 ### 表结构要求 所有业务表必须包含以下字段: ```prisma model YourModel { id Int @id @default(autoincrement()) tenantId Int @map("tenant_id") /// 租户ID(必填) // 业务字段... name String description String? // 审计字段 validState Int @default(1) @map("valid_state") /// 1-有效,2-失效 creator Int? /// 创建人ID modifier Int? /// 修改人ID createTime DateTime @default(now()) @map("create_time") modifyTime DateTime @updatedAt @map("modify_time") // 关系 tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) creatorUser User? @relation("YourModelCreator", fields: [creator], references: [id], onDelete: SetNull) modifierUser User? @relation("YourModelModifier", fields: [modifier], references: [id], onDelete: SetNull) @@map("your_table_name") } ``` ### 字段命名规范 - Prisma 模型使用 camelCase:`tenantId`, `createTime` - 数据库列使用 snake_case:`tenant_id`, `create_time` - 使用 `@map()` 映射字段名 - 使用 `@@map()` 映射表名 ### 状态字段 使用 `validState` 表示数据有效性: - `1` - 有效 - `2` - 失效(软删除) ```prisma validState Int @default(1) @map("valid_state") ``` ## 关系设计 ### 一对多关系 ```prisma model Tenant { id Int @id @default(autoincrement()) users User[] } model User { id Int @id @default(autoincrement()) tenantId Int @map("tenant_id") tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade) } ``` ### 多对多关系 使用显式中间表: ```prisma model User { id Int @id @default(autoincrement()) roles UserRole[] } model Role { id Int @id @default(autoincrement()) users UserRole[] } model UserRole { userId Int @map("user_id") roleId Int @map("role_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) @@id([userId, roleId]) @@map("user_roles") } ``` ### 一对一关系 ```prisma model User { id Int @id @default(autoincrement()) teacher Teacher? } model Teacher { id Int @id @default(autoincrement()) userId Int @unique @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) } ``` ### 级联删除规则 - 强依赖关系:`onDelete: Cascade` - 弱依赖关系:`onDelete: SetNull`(字段必须可选) - 保护性关系:`onDelete: Restrict` ## 索引设计 ### 自动索引 - 主键自动创建索引 - 外键字段自动创建索引 - `@unique` 字段自动创建唯一索引 ### 复合索引 ```prisma model User { tenantId Int username String @@unique([tenantId, username]) @@index([tenantId, validState]) } ``` ### 性能优化索引 为频繁查询的字段添加索引: ```prisma model Contest { tenantId Int status Int startTime DateTime @@index([tenantId, status]) @@index([tenantId, startTime]) } ``` ## Prisma 查询最佳实践 ### 使用 include 预加载关联 避免 N+1 查询问题: ```typescript // ✅ 好的做法 - 使用 include 预加载 const users = await prisma.user.findMany({ where: { tenantId }, include: { roles: { include: { role: true, }, }, }, }); // ❌ 不好的做法 - N+1 查询 const users = await prisma.user.findMany({ where: { tenantId }, }); for (const user of users) { user.roles = await prisma.userRole.findMany({ where: { userId: user.id }, }); } ``` ### 使用 select 精简字段 只查询需要的字段: ```typescript const users = await prisma.user.findMany({ where: { tenantId }, select: { id: true, username: true, nickname: true, // 不查询 password 等敏感字段 }, }); ``` ### 分页查询 ```typescript const users = await prisma.user.findMany({ where: { tenantId, validState: 1 }, skip: (page - 1) * pageSize, take: pageSize, orderBy: { createTime: "desc" }, }); const total = await prisma.user.count({ where: { tenantId, validState: 1 }, }); ``` ### 事务处理 使用 `$transaction` 确保数据一致性: ```typescript await prisma.$transaction(async (tx) => { // 创建用户 const user = await tx.user.create({ data: { username: "test", tenantId, }, }); // 创建用户角色关联 await tx.userRole.createMany({ data: roleIds.map((roleId) => ({ userId: user.id, roleId, })), }); }); ``` ## 数据迁移 ### 创建迁移 ```bash # 开发环境 - 创建并应用迁移 pnpm prisma:migrate:dev # 生产环境 - 只应用迁移 pnpm prisma:migrate:deploy ``` ### 迁移命名 使用描述性的迁移名称: ```bash prisma migrate dev --name add_contest_module prisma migrate dev --name add_user_avatar_field ``` ### 迁移注意事项 - 迁移前备份数据库 - 测试迁移在开发环境的执行 - 生产环境使用 `migrate deploy` 而不是 `migrate dev` - 不要手动修改已应用的迁移文件 ## 性能优化清单 - [ ] 频繁查询的字段添加了索引 - [ ] 使用 `include` 预加载关联数据 - [ ] 使用 `select` 只查询需要的字段 - [ ] 实现了分页查询 - [ ] 复杂操作使用了事务 - [ ] 避免了 N+1 查询问题