6月2日 01:49
TypeORM 软删除怎么用?@DeleteDateColumn 配置和唯一约束冲突解决
软删除不是真的删除数据,而是标记 deletedAt 字段,查询时自动过滤已删除的记录。TypeORM 内置支持软删除,一行配置开启。
开启软删除
在实体上加 @DeleteDateColumn():
typescript@Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @DeleteDateColumn() deletedAt: Date; // null = 未删除,有值 = 已删除 }
delete 操作变成 UPDATE:
typescriptawait userRepo.softDelete(1); // SQL: UPDATE user SET "deletedAt" = NOW() WHERE id = 1 await userRepo.remove(user); // 同样效果,设置 deletedAt 而不是 DELETE
find 自动排除已删除记录:
typescriptconst users = await userRepo.find(); // SQL: SELECT * FROM user WHERE "deletedAt" IS NULL
查询包含已删除的记录
typescript// 包含已删除的记录 const allUsers = await userRepo.find({ withDeleted: true }); // 只查已删除的记录 const deletedUsers = await userRepo.find({ where: { deletedAt: Not(IsNull()) }, withDeleted: true });
withDeleted: true 告诉 TypeORM 不要加 WHERE "deletedAt" IS NULL 条件。
恢复已删除的记录
typescriptawait userRepo.restore(1); // SQL: UPDATE user SET "deletedAt" = NULL WHERE id = 1
restore 把 deletedAt 设回 NULL,记录恢复正常。
软删除的坑
1. 唯一约束冲突
软删除后,唯一字段(如 email)仍然占用唯一约束。删除 user@email.com 后,新建同名用户会报唯一冲突。
解决方案:唯一约束包含 deletedAt。
typescript@Entity() @Unique(['email', 'deletedAt']) export class User { @Column() email: string; @DeleteDateColumn() deletedAt: Date; }
但这样未删除记录的 deletedAt 是 NULL,多条 NULL 在 PostgreSQL 的唯一约束里不冲突(符合 SQL 标准),MySQL 则需要用条件索引或去掉唯一约束。
2. 关联数据不会级联软删除
typescript@OneToMany(() => Post, post => post.author) posts: Post[];
软删除 User 时,Post 不会被自动软删除。需要手动处理:
typescriptasync function softDeleteUser(id: number) { await postRepo.softDelete({ authorId: id }); await userRepo.softDelete(id); }
3. 物理删除仍然是需要的
软删除的数据积累会膨胀表。定期清理:
typescript// 物理删除 30 天前软删除的记录 await userRepo .createQueryBuilder() .delete() .where("deletedAt < :date", { date: new Date(Date.now() - 30 * 86400000) }) .execute();
不用软删除的替代方案
- 事件溯源:不删除数据,而是追加"已删除"事件。查询时通过事件回放构建当前状态
- 归档表:DELETE 前把数据 INSERT 到归档表,然后物理删除。查询只看主表,归档表做审计
软删除适合"需要恢复、需要审计追踪"的场景。如果只是为了安全,备份 + 物理删除更干净。