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:

typescript
await userRepo.softDelete(1); // SQL: UPDATE user SET "deletedAt" = NOW() WHERE id = 1 await userRepo.remove(user); // 同样效果,设置 deletedAt 而不是 DELETE

find 自动排除已删除记录:

typescript
const 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 条件。

恢复已删除的记录

typescript
await userRepo.restore(1); // SQL: UPDATE user SET "deletedAt" = NULL WHERE id = 1

restoredeletedAt 设回 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 不会被自动软删除。需要手动处理:

typescript
async 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 到归档表,然后物理删除。查询只看主表,归档表做审计

软删除适合"需要恢复、需要审计追踪"的场景。如果只是为了安全,备份 + 物理删除更干净。

标签:TypeORM