4-10 进阶完成用户更新逻辑(嵌套关联关系)
概述
本节完成用户更新接口中涉及的多层嵌套关联关系处理,包括:角色与权限(Role-Permission)的更新、用户与角色(User-Role)的更新。核心难点在于如何正确处理 Prisma 的嵌套更新逻辑,以及如何利用 deleteMany + create 实现关联数据的先删后建模式。
更新逻辑架构
用户更新涉及两层嵌套关联关系:
User
├── UserRole[] ← 第一层关联:用户与角色
│ └── Role
│ └── RolePermission[] ← 第二层关联:角色与权限
│ └── Permission
text
更新顺序:
- 先更新 Role-Permission 关联(并行处理所有角色)
- 再更新 User-Role 关联(在用户更新中一并处理)
将 Role-Permission 更新放在外侧独立处理,而与 User-Role 更新分开,原因是两者的关联关系是独立的逻辑单元,混在一起会导致代码嵌套过深、可读性差。
密码哈希处理
更新用户时,如果提交了新密码,需要先进行哈希处理:
// user.service.ts
async update(whereCondition: any, updateData: any) {
const updateDate: any = {};
// 密码哈希处理
if (updateData.password) {
const newHashPass = await argon2.hash(updateData.password);
updateData.password = newHashPass;
}
const roleIds: number[] = [];
// 并行处理角色权限更新
await Promise.all(
updateData.roles.map(async (role) => {
const { permissions, ...rest } = role;
// 更新角色(含权限)
const updatedRole = await prisma.role.update({
where: { id: role.id },
data: {
...rest,
rolePermissions: {
// 先删除所有旧的权限关联
deleteMany: {},
// 再创建新的权限关联
create: (permissions || []).map((permission) => ({
permission: {
connectOrCreate: {
where: { name: permission.name },
create: permission,
},
},
})),
},
},
});
roleIds.push(updatedRole.id);
}),
);
// ...后续用户更新逻辑
}
typescript
关键 API 说明
| Prisma API | 用途 | 说明 |
|---|---|---|
deleteMany: {} | 删除关联表所有记录 | 传递空对象表示删除该关联表中的所有记录 |
create: [...] | 批量创建关联记录 | 在删除旧记录后创建新的关联关系 |
connectOrCreate | 连接或创建 | 如果 Permission 已存在则连接,不存在则创建 |
用户与角色的嵌套更新
角色权限更新完成后,处理用户本身的更新以及用户-角色关联:
// 用户更新(含角色关联)
const updatedUser = await prisma.user.update({
where: whereCondition,
data: {
...updateData,
...rest, // 其他字段
userRoles: {
// 先删除所有旧的角色关联
deleteMany: {},
// 再创建新的角色关联
create: roleIds.map((roleId) => ({
roleId,
})),
},
},
include: {
userRoles: {
include: {
role: true, // 包含角色详细信息
},
},
},
});
return updatedUser;
typescript
connectOrCreate 的精妙用法
在更新 Role-Permission 关联时,connectOrCreate 允许同时处理两种情况:
- 已有权限:通过
where: { name }匹配并连接 - 新权限:通过
create创建新的 Permission 记录并关联
{
permission: {
connectOrCreate: {
where: { name: permission.name }, // 查找已有权限
create: { name: permission.name, action: permission.action }, // 不存在则创建
},
},
}
typescript
这意味着客户端可以传入尚未存在于数据库中的新权限,系统会自动创建并建立关联。
DTO Transform 中的 Bug 排查
在处理 Permission 的 DTO 转换时,需要注意 split 方法对原始 name 的破坏:
// ❌ 错误写法:split 会破坏 name
@Transform(({ value }) =>
value.map((item: string) => ({
name: item.split('_')[0], // name 被截断
action: item.split('_')[1],
}))
)
// ✅ 正确写法:name 保持完整,action 从 name 中提取
@Transform(({ value }) =>
value.map((item: string) => {
const parts = item.split('_');
return {
name: item, // name 保持原值(唯一标识)
action: parts[1] || '', // action 从中提取
};
})
)
typescript
排错思路:当遇到 Unique constraint failed on the fields: roleId, permissionId 错误时,说明关联表中已存在相同的记录。此时需要检查 deleteMany 是否正确执行,以及数据结构是否符合预期。
响应序列化:排除敏感信息
更新完成后,需要通过序列化拦截器排除敏感字段(如 password),并格式化响应数据:
// dto/public-update-user.dto.ts
import { Expose, Exclude, Transform, Type } from 'class-transformer';
import { UpdateUserDto } from './update-user.dto';
export class PublicUpdateUserDto extends UpdateUserDto {
@Exclude()
password: string;
id: number;
username: string;
@Expose({ name: 'userRoles' })
@Type(() => Object)
@Transform(({ value }) =>
value?.map((item: any) => item.roleId) || []
)
roleIds: number[];
}
typescript
// user.controller.ts
import { ClassSerializerInterceptor } from '@nestjs/common';
@Put(':id')
@SerializeOptions({ type: PublicUpdateUserDto })
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.userService.update({ id: +id }, updateUserDto);
}
typescript
嵌套 Include 获取完整数据
如果需要在响应中包含完整的角色-权限链路,可以在 update 查询中添加深层嵌套的 include:
const updatedUser = await prisma.user.update({
where: whereCondition,
data: { /* ... */ },
include: {
userRoles: {
include: {
role: {
include: {
rolePermissions: {
include: {
permission: true, // 包含完整的权限信息
},
},
},
},
},
},
},
});
typescript
完整的更新链路验证
通过测试验证整个更新流程:
| 测试步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 清空用户 permissions,指定角色 ID 14 | user_role 表中出现 userId=1, roleId=14 |
| 2 | 为角色 14 添加已有权限 user_read | role_permission 表中 roleId=14 新增一条记录 |
| 3 | 为角色添加不存在的权限 user_create | permission 表自动创建新记录,role_permission 关联成功 |
| 4 | 传入多个角色(字符串数组形式) | 唯一约束报错 → 修复 DTO transform 后正常 |
| 5 | 序列化验证 | 响应中不包含 password,包含角色 ID 列表 |
下一步:RBAC 守卫
至此,从用户到角色再到权限的完整链路已打通。下一步是在 Guard 中实现权限匹配逻辑:从数据库读取用户的角色和权限,与路由装饰器中指定的权限字符串进行比对,决定是否允许访问。
↑