279 lines
5.5 KiB
Plaintext
279 lines
5.5 KiB
Plaintext
|
|
---
|
|||
|
|
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 查询问题
|